/**
 *	Image Map Editor (imgmap) - in-browser imagemap editor
 *	Copyright (C) 2006 - 2008 Adam Maschek (adam.maschek @ gmail.com)
 *
 *	This program is free software; you can redistribute it and/or
 *	modify it under the terms of the GNU General Public License
 *	as published by the Free Software Foundation; either version 2
 *	of the License, or (at your option) any later version.
 *
 *	This program 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 General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this program; if not, write to the Free Software
 *	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 *	@fileoverview
 *	Online Image Map Editor - main script file.
 *	This is the main script file of the Online Image Map Editor.
 *
 *	TODO:
 *	-scriptload race condition fix
 *	-destroy/cleanup function ?
 *	-testing highlighter
 *	-cursor area_mousemove in opera not refreshing quite well - bug reported
 *	-get rid of memo array
 *	-highlight which control point is edited in html or form mode
 *	-more comments, especially on config vars
 *	-make function names more logical
 *	- dumpconfig
 *	-prepare for bad input /poly not properly closed?
 *	-prepare for % values in coords
 *	-prepare for default shape http://www.w3.org/TR/html4/struct/objects.html#edef-AREA
 *
 *	@date	26-02-2007 2:24:50
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@copyright
 *	@version 2.2
 *
 */
/*jslint browser: true, newcap: false, white: false, onevar: false, plusplus: false, eqeqeq: false, nomen: false */
/*global imgmapStrings:true, window:false, G_vmlCanvasManager:false, air:false, imgmap_spawnObjects:true */
/**
 *	@author	Adam Maschek
 *	@constructor
 *	@param config	The config object.
 */
function imgmap(config) {

	/** Version string of imgmap */
	this.version = "2.2";

	/** Build date of imgmap */
	this.buildDate = "2009/08/12 22:18";

	/** Sequential build number of imgmap */
	this.buildNumber = "113";

	/** Config object of the imgmap instance */
	this.config = {};

	/** Status flag to indicate current drawing mode */
	this.is_drawing = 0;

	/**	Array to hold language strings */
	this.strings   = [];

	/** Helper array for some drawing operations */
	this.memory    = [];

	/**	Array to hold reference to all areas (canvases) */
	this.areas     = [];

	/**	Array to hold last log entries */
	this.logStore  = [];

	/**	Associative array to hold bound event handlers */
	this.eventHandlers  = {};

	this.currentid = 0;
	this.draggedId  = null;
	this.selectedId = null;
	this.nextShape = 'rect';

	/** possible values: 0 - edit, 1 - preview */
	this.viewmode  = 0;

	/** array of dynamically loaded javascripts */
	this.loadedScripts = [];
	this.isLoaded   = false;
	this.cntReloads = 0;

	/**	holds the name of the actively edited map, use getMapName to read it */
	this.mapname    = '';

	/**	holds the id of the actively edited map, use getMapIdto read it */
	this.mapid      = '';

	/** watermark to attach to output */
	this.waterMark  = '<!-- Created by Online Image Map Editor (http://www.maschek.hu/imagemap/index) -->';

	/** global scale of areas (1-normal, 2-doubled, 0.5-half, etc.) */
	this.globalscale = 1;

	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_DRAW          = 1;
	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_MOVE          = 11;
	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_RESIZE_TOP    = 12;
	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_RESIZE_RIGHT  = 13;
	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_RESIZE_BOTTOM = 14;
	/** is_drawing draw mode constant */
	this.DM_RECTANGLE_RESIZE_LEFT   = 15;

	/** is_drawing draw mode constant */
	this.DM_SQUARE_DRAW             = 2;
	/** is_drawing draw mode constant */
	this.DM_SQUARE_MOVE             = 21;
	/** is_drawing draw mode constant */
	this.DM_SQUARE_RESIZE_TOP       = 22;
	/** is_drawing draw mode constant */
	this.DM_SQUARE_RESIZE_RIGHT     = 23;
	/** is_drawing draw mode constant */
	this.DM_SQUARE_RESIZE_BOTTOM    = 24;
	/** is_drawing draw mode constant */
	this.DM_SQUARE_RESIZE_LEFT      = 25;

	/** is_drawing draw mode constant */
	this.DM_POLYGON_DRAW            = 3;
	/** is_drawing draw mode constant */
	this.DM_POLYGON_LASTDRAW        = 30;
	/** is_drawing draw mode constant */
	this.DM_POLYGON_MOVE            = 31;

	/** is_drawing draw mode constant */
	this.DM_BEZIER_DRAW             = 4;
	/** is_drawing draw mode constant */
	this.DM_BEZIER_LASTDRAW         = 40;
	/** is_drawing draw mode constant */
	this.DM_BEZIER_MOVE             = 41;

	//set some config defaults below

	/**
	 *	Mode of operation
	 *	possible values:
	 *	editor - classical editor,
	 *	editor2 - dreamweaver style editor,
	 *	highlighter - map highlighter, will spawn imgmap instances for each map found in the current page
	 *	highlighter_spawn - internal mode after spawning imgmap objects
	 */
	this.config.mode     = "editor";

	this.config.baseroot    = '';
	this.config.lang        = '';
	this.config.defaultLang = 'en';
	this.config.loglevel    = 0;
	this.config.custom_callbacks = {};//possible values: see below!

	/**	Callback events that you can handle in your GUI. */
	this.event_types        = [
		'onModeChanged',
		'onHtmlChanged',
		'onAddArea',
		'onRemoveArea',
		'onDrawArea',
		'onResizeArea',
		'onRelaxArea',
		'onFocusArea',
		'onBlurArea',
		'onMoveArea',
		'onSelectRow',
		'onLoadImage',
		'onSetMap',
		'onGetMap',
		'onSelectArea',
		'onDblClickArea',
		'onStatusMessage',
		'onAreaChanged'];

	//default color values
	this.config.CL_DRAW_BOX        = '#E32636';
	this.config.CL_DRAW_SHAPE      = '#d00';
	this.config.CL_DRAW_BG         = '#fff';
	this.config.CL_NORM_BOX        = '#E32636';
	this.config.CL_NORM_SHAPE      = '#d00';
	this.config.CL_NORM_BG         = '#fff';
	this.config.CL_HIGHLIGHT_BOX   = '#E32636';
	this.config.CL_HIGHLIGHT_SHAPE = '#d00';
	this.config.CL_HIGHLIGHT_BG    = '#fff';
	this.config.CL_KNOB            = '#555';

	this.config.bounding_box       = true;
	this.config.label              = '%n';
	//the format string of the area labels - possible values: %n - number, %c - coords, %h - href, %a - alt, %t - title

	this.config.label_class        = 'imgmap_label';
	//the css class to apply on labels
	this.config.label_style        = 'font: bold 10px Arial';
	//this.config.label_style        = 'font-weight: bold; font-size: 10px; font-family: Arial; color: #964';
	//the css style(s) to apply on labels

	this.config.hint               = '#%n %h';
	//the format string of the area mouseover hints - possible values: %n - number, %c - coords, %h - href, %a - alt, %t - title

	this.config.draw_opacity       = '35';
	//the opacity value of the area while drawing, moving or resizing - possible values 0 - 100 or range "(x)-y"
	this.config.norm_opacity       = '50';
	//the opacity value of the area while relaxed - possible values 0 - 100	or range "(x)-y"
	this.config.highlight_opacity  = '70';
	//the opacity value of the area while highlighted - possible values 0 - 100 or range "(x)-y"
	this.config.cursor_default     = 'crosshair';		//auto/pointer
	//the css cursor while hovering over the image

	//browser sniff
	var ua = navigator.userAgent;
	this.isMSIE    = (navigator.appName == "Microsoft Internet Explorer");
	this.isMSIE5   = this.isMSIE && (ua.indexOf('MSIE 5')   != -1);
	this.isMSIE5_0 = this.isMSIE && (ua.indexOf('MSIE 5.0') != -1);
	this.isMSIE7   = this.isMSIE && (ua.indexOf('MSIE 7')   != -1);
	this.isGecko   = ua.indexOf('Gecko')  != -1;
	this.isSafari  = ua.indexOf('Safari') != -1;
	this.isOpera   = (typeof window.opera != 'undefined');

	this.setup(config);
}


/**
 *	Return an object given by id or object itself.
 *	@date	22-02-2007 0:14:50
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	objorid	A DOM object, or id of a DOM object.
 *	@return	The identified DOM object or null on error.
 */
imgmap.prototype.assignOID = function(objorid) {
	try {
		if (typeof objorid == 'undefined') {
			this.log("Undefined object passed to assignOID.");// Called from: " + arguments.callee.caller, 1);
			return null;
		}
		else if (typeof objorid == 'object') {
			return objorid;
		}
		else if (typeof objorid == 'string') {
			return document.getElementById(objorid);
		}
	}
	catch (err) {
		this.log("Error in assignOID", 1);
	}
	return null;
};


/**
 *	Main setup function.
 *	Can be called manually or constructor will call it.
 *	@date	22-02-2007 0:15:42
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	config	config object
 *	@return	True if all went ok.
 */
imgmap.prototype.setup = function(config) {
	//this.log('setup');

	//copy non-default config parameters to this.config
	for (var i in config) {
		if (config.hasOwnProperty(i)) {
			this.config[i] = config[i];
		}
	}

	//set document event hooks
	this.addEvent(document, 'keydown',   this.eventHandlers.doc_keydown = this.doc_keydown.bind(this));
	this.addEvent(document, 'keyup',     this.eventHandlers.doc_keyup = this.doc_keyup.bind(this));
	this.addEvent(document, 'mousedown', this.eventHandlers.doc_mousedown = this.doc_mousedown.bind(this));

	//set pic_container element - supposedly it already exists in the DOM
	if (config && config.pic_container) {
		this.pic_container = this.assignOID(config.pic_container);
		this.disableSelection(this.pic_container);
	}

	if (!this.config.baseroot) {
		//search for a base - theoretically there can only be one, but lets search
		//for the first non-empty
		var bases = document.getElementsByTagName('base');
		var base  = '';
		for (i=0; i<bases.length; i++) {//i declared earlier
			if (bases[i].href) {
				base = bases[i].href;
				//append slash if missing
				if (base.charAt(base.length-1) != '/') {
					base+= '/';
				}
				break;
			}
		}
		//search for scripts
		var scripts = document.getElementsByTagName('script');
		var found = false;
		for (i=0; i<scripts.length; i++) {//i declared earlier
			if (scripts[i].src && scripts[i].src.match(/imgmap\w*\.js(\?.*?)?$/)) {
				var src = scripts[i].src;
				//cut filename part, leave last slash
				src = src.substring(0, src.lastIndexOf('/') + 1);
				//set final baseroot path
				if (base && src.indexOf('://') == -1) {
					this.config.baseroot = base + src;
				}
				else {
					this.config.baseroot = src;
				}
				//exit loop
				found = true;
				break;
			}
		}
		if (!found && this.config.baseroot === '') {
			this.log("Unable to detect baseroot.", 1);
		}

	}

	//load excanvas js - as soon as possible
	if (this.isMSIE &&
		typeof window.CanvasRenderingContext2D == 'undefined' && typeof G_vmlCanvasManager == 'undefined') {
		this.loadScript(this.config.baseroot + 'excanvas.js');
		//alert('loadcanvas');
	}
	//alert(this.config.baseroot);

	//load language js - as soon as possible
	if (!this.config.lang) {
		this.config.lang = this.detectLanguage();
	}
	if (typeof imgmapStrings == 'undefined') {
		//language file might have already been loaded (ex highlighter mode)
		this.loadScript(this.config.baseroot + 'lang_' + this.config.lang + '.js');
	}

	//check event hooks
	var found, j, le;
	for (i in this.config.custom_callbacks) {
		if (this.config.custom_callbacks.hasOwnProperty(i)) {
			found = false;
			for (j = 0, le = this.event_types.length; j < le; j++) {
				if (i == this.event_types[j]) {
					found = true;
					break;
				}
			}
			if (!found) {
				this.log("Unknown custom callback: " + i, 1);
			}
		}
	}

	//hook onload event - as late as possible
	this.addEvent(window, 'load', this.onLoad.bind(this));
	return true;
};


/**
 *	currently unused
 *	@ignore
 */
imgmap.prototype.retryDelayed = function(fn, delay, tries) {
	if (typeof fn.tries == 'undefined') {fn.tries = 0;}
	//alert(fn.tries+1);
	if (fn.tries++ < tries) {
		//alert('ss');
		window.setTimeout(function() {
		fn.apply(this);
		}, delay);
	}
};


/**
 *	EVENT HANDLER: Handle event when the page with scripts is loaded.
 *	@date	22-02-2007 0:16:22
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	e	The event object.
 */
imgmap.prototype.onLoad = function(e) {
	if (this.isLoaded) {return true;}
	var _this = this;
	//this.log('readystate: ' +  document.readyState);
	if (typeof imgmapStrings == 'undefined') {
		if (this.cntReloads++ < 5) {
			//this.retryDelayed(_this.onLoad(), 1000, 3);
			window.setTimeout(function () {_this.onLoad(e);} ,1200);
			this.log('Delaying onload (language ' + this.config.lang + ' not loaded, try: ' + this.cntReloads + ')');
			return false;
		}
		else if (this.config.lang != this.config.defaultLang && this.config.defaultLang != 'en') {
			this.log('Falling back to default language: ' + this.config.defaultLang);
			this.cntReloads = 0;
			this.config.lang = this.config.defaultLang;
			this.loadScript(this.config.baseroot + 'lang_' + this.config.lang + '.js');
			window.setTimeout(function () {_this.onLoad(e);} ,1200);
			return false;
		}
		else if (this.config.lang != 'en') {
			this.log('Falling back to english language');
			this.cntReloads = 0;
			this.config.lang = 'en';
			this.loadScript(this.config.baseroot + 'lang_' + this.config.lang + '.js');
			window.setTimeout(function () {_this.onLoad(e);} ,1200);
			return false;
		}
	}
	//else
	try {
		this.loadStrings(imgmapStrings);
	}
	catch (err) {
		this.log("Unable to load language strings", 1);
	}

	//check if ExplorerCanvas correctly loaded - detect if browser supports canvas
	//alert(typeof G_vmlCanvasManager + this.isMSIE + typeof window.CanvasRenderingContext2D);
	if (this.isMSIE) {
		//alert('cccc');
		//alert(typeof G_vmlCanvasManager);
		if (typeof window.CanvasRenderingContext2D == 'undefined' && typeof G_vmlCanvasManager == 'undefined') {
			//alert('bbb');
			/*
			if (this.cntReloads++ < 5) {
				var _this = this;
				//this.retryDelayed(_this.onLoad(), 1000, 3);
				window.setTimeout(function () {
					_this.onLoad(e);
					}
					,1000
					);
				//alert('aaa');
				this.log('Delaying onload (excanvas not loaded, try: ' + this.cntReloads + ')');
				return false;
			}
			*/
			this.log(this.strings.ERR_EXCANVAS_LOAD, 2);//critical error
		}
	}

	if (this.config.mode == 'highlighter') {
		//call global scope function
		imgmap_spawnObjects(this.config);
	}
	this.isLoaded = true;
	return true;
};


/**
 *	Attach new 'evt' event handler 'callback' to 'obj'
 *	@date	24-02-2007 21:16:20
 *	@param	obj	The object on which the handler is defined.
 *	@param	evt	The name of the event, like mousedown.
 *	@param	callback	The callback function (named if you want it to be removed).
 */
imgmap.prototype.addEvent = function(obj, evt, callback) {
	if (obj.attachEvent) {
		//Microsoft style registration model
		return obj.attachEvent("on" + evt, callback);
	}
	else if (obj.addEventListener) {
		//W3C style model
		obj.addEventListener(evt, callback, false);
		return true;
	}
	else {
		obj['on' + evt] = callback;
	}
};


/**
 *	Detach 'evt' event handled by 'callback' from 'obj' object.
 *	Callback must be a non anonymous function, see eventHandlers.
 *	@see	#eventHandlers
 *	Example: myimgmap.removeEvent(myimgmap.pic, 'mousedown', myimgmap.eventHandlers.img_mousedown);
 *	@date	24-11-2007 15:22:17
 *	@param	obj	The object on which the handler is defined.
 *	@param	evt	The name of the event, like mousedown.
 *	@param	callback	The named callback function.
 */
imgmap.prototype.removeEvent = function(obj, evt, callback) {
	if (obj.detachEvent) {
		//Microsoft style detach model
		return obj.detachEvent("on" + evt, callback);
	}
	else if (obj.removeEventListener) {
		//W3C style model
		obj.removeEventListener(evt, callback, false);
		return true;
	}
	else {
		obj['on' + evt] = null;
	}
};


/**
 *	We need this because load events for scripts function slightly differently.
 *	@link	http://dean.edwards.name/weblog/2006/06/again/
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	24-03-2007 11:02:21
 */
imgmap.prototype.addLoadEvent = function(obj, callback) {
	if (obj.attachEvent) {
		//Microsoft style registration model
		return obj.attachEvent("onreadystatechange", callback);
	}
	else if (obj.addEventListener) {
		//W3C style registration model
		obj.addEventListener('load', callback, false);
		return true;
	}
	else {
		obj.onload = callback;
	}
};


/**
 *	Include another js script into the current document.
 *	@date	22-02-2007 0:17:04
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	url	The url of the script we want to load.
 *	@see	#script_load
 *	@see	#addLoadEvent
 */
imgmap.prototype.loadScript = function(url) {
	if (url === '') {return false;}
	if (this.loadedScripts[url] == 1) {return true;}//script already loaded
	this.log('Loading script: ' + url);
	//we might need this someday for safari?
	//var temp = '<script language="javascript" type="text/javascript" src="' + url + '"></script>';
	//document.write(temp);
	try {
		var head = document.getElementsByTagName('head')[0];
		var temp = document.createElement('SCRIPT');
		temp.setAttribute('language', 'javascript');
		temp.setAttribute('type', 'text/javascript');
		temp.setAttribute('src', url);
		//temp.setAttribute('defer', true);
		head.appendChild(temp);
		this.addLoadEvent(temp, this.script_load.bind(this));
	}
	catch (err) {
		this.log('Error loading script: ' + url);
	}
	return true;
};


/**
 *	EVENT HANDLER: Event handler of external script loaded.
 *	@param	e	The event object.
 */
imgmap.prototype.script_load = function(e) {
	var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
	var url = obj.src;
	var complete = false;
	if (typeof obj.readyState != 'undefined') {
		//explorer
		if (obj.readyState == 'complete' || obj.readyState == 'loaded') {
			complete = true;
		}
	}
	else {
		//other browsers?
		complete = true;
	}
	if (complete) {
		this.loadedScripts[url] = 1;
		this.log('Loaded script: ' + url);
		return true;
	}
};


/**
 *	Load strings from a key:value object to the prototype strings array.
 *	@author adam
 *	@date	2007
 *	@param	obj	Javascript object that holds key:value pairs.
 */
imgmap.prototype.loadStrings = function(obj) {
	for (var key in obj) {
		if (obj.hasOwnProperty(key)) {
			this.strings[key] = obj[key];
		}
	}
};


/**
 *	This function is to load a given img url to the pic_container.
 *
 *	Loading an image will clear all current maps.
 *	@see	#useImage
 *	@param img The imageurl or object to load (if object, function will get url, and do a recall)
 *	@param imgw The width we want to force on the image	(optional)
 *	@param imgh The height we want to force on the image (optional)
 *	@returns True on success
 */
imgmap.prototype.loadImage = function(img, imgw, imgh) {
	//test for container
	if (typeof this.pic_container == 'undefined') {
		this.log('You must have pic_container defined to use loadImage!', 2);
		return false;
	}
	//wipe all
	this.removeAllAreas();
	//reset scale
	this.globalscale = 1;
	this.fireEvent('onHtmlChanged', '');//empty
	if (!this._getLastArea()) {
		//init with one new area if there was none editable
		if (this.config.mode != "editor2") {this.addNewArea();}
	}
	if (typeof img == 'string') {
		//there is an image given with url to load
		if (typeof this.pic == 'undefined') {
			this.pic = document.createElement('IMG');
			this.pic_container.appendChild(this.pic);
			//event handler hooking - only at the first load
			this.addEvent(this.pic, 'mousedown', this.eventHandlers.img_mousedown = this.img_mousedown.bind(this));
			this.addEvent(this.pic, 'mouseup',   this.eventHandlers.img_mouseup = this.img_mouseup.bind(this));
			this.addEvent(this.pic, 'mousemove', this.eventHandlers.img_mousemove = this.img_mousemove.bind(this));
			this.pic.style.cursor = this.config.cursor_default;
		}
		//img ='../../'+img;
		//this.log('Loading image: ' + img, 0);
		//calculate timestamp to bypass browser cache mechanism
		var q = '?';
		if (img.indexOf('?') > -1) {
			q = '&';
		}
		this.pic.src = img + q + (new Date().getTime());
		if (imgw && imgw > 0) {this.pic.setAttribute('width',  imgw);}
		if (imgh && imgh > 0) {this.pic.setAttribute('height', imgh);}
		this.fireEvent('onLoadImage', this.pic);
		return true;
	}
	else if (typeof img == 'object') {
		//we have to use the src of the image object
		var src = img.src; //img.getAttribute('src');
		if (src === '' && img.getAttribute('mce_src') !== '') {
			//if it is a tinymce object, it has no src but mce_src attribute!
			src = img.getAttribute('mce_src');
		}
		else if (src === '' && img.getAttribute('_fcksavedurl') !== '') {
			//if it is an fck object, it might have only _fcksavedurl attribute!
			src = img.getAttribute('_fcksavedurl');
		}
		// Get the displayed dimensions of the image
		if (!imgw) {
			imgw = img.clientWidth;
		}
		if (!imgh) {
			imgh = img.clientHeight;
		}
		//recurse, this time with the url string
		return this.loadImage(src, imgw, imgh);
	}
};


/**
 *	We use this when there is an existing image object we want to handle with imgmap.
 *	Mainly used in highlighter mode.
 *	@author	adam
 *	@see	#loadImage
 *	@see	#imgmap_spawnObjects
 *	@date	2007
 *	@param	img	DOM object or id of image we want to use.
 */
imgmap.prototype.useImage = function(img) {
	//wipe all
	this.removeAllAreas();
	if (!this._getLastArea()) {
		//init with one new area if there was none editable
		if (this.config.mode != "editor2") {this.addNewArea();}
	}
	img = this.assignOID(img);
	if (typeof img == 'object') {
		if (typeof this.pic != 'undefined') {
			//remove previous handlers
			this.removeEvent(this.pic, 'mousedown', this.eventHandlers.img_mousedown);
			this.removeEvent(this.pic, 'mouseup',   this.eventHandlers.img_mouseup);
			this.removeEvent(this.pic, 'mousemove', this.eventHandlers.img_mousemove);
			this.pic.style.cursor = '';
		}
		this.pic = img;
		//hook event handlers
		this.addEvent(this.pic, 'mousedown', this.eventHandlers.img_mousedown = this.img_mousedown.bind(this));
		this.addEvent(this.pic, 'mouseup',   this.eventHandlers.img_mouseup = this.img_mouseup.bind(this));
		this.addEvent(this.pic, 'mousemove', this.eventHandlers.img_mousemove = this.img_mousemove.bind(this));
		this.pic.style.cursor = this.config.cursor_default;

		if (this.pic.parentNode.className == 'pic_container') {
			//use existing container
			this.pic_container = this.pic.parentNode;
		}
		else {
			//dynamically create container
			this.pic_container = document.createElement("div");
			this.pic_container.className = 'pic_container';
			this.pic.parentNode.insertBefore(this.pic_container, this.pic);
			//ref: If the node already exists it is removed from current parent node, then added to new parent node.
			this.pic_container.appendChild(this.pic);
		}

		this.fireEvent('onLoadImage', this.pic);
		return true;
	}
};


/**
 *	Fires custom hook onStatusMessage, passing the status string.
 *	Use this to update your GUI.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	26-07-2008 13:22:54
 *	@param	str	The status string
 */
imgmap.prototype.statusMessage = function(str) {
	this.fireEvent('onStatusMessage', str);
};


/**
 *	Adds basic logging functionality using firebug console object if available.
 *	Also tries to use AIR introspector if available.
 *	@date	20-02-2007 17:55:18
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	obj	The object or string you want to debug/echo.
 *	@level	level	The log level, 0 being the smallest issue.
 */
imgmap.prototype.log = function(obj, level) {
/*
	if (level === '' || typeof level == 'undefined') {level = 0;}
	if (this.config.loglevel != -1 && level >= this.config.loglevel) {
		this.logStore.push({level: level, obj: obj});
	}
	if (typeof console == 'object') {
		console.log(obj);
	}
	else if (this.isOpera) {
		opera.postError(level + ': ' + obj);
	}
	else if (typeof air == 'object') {
		//we are inside AIR
		if (typeof air.Introspector == 'object') {
			air.Introspector.Console.log(obj);
		}
	else {
			air.trace(obj);
		}
	}
	else {
		if (level > 1) {
			//alert(level + ': ' + obj);
			//dump with all pevious errors:
			var msg = '';
			for (var i=0, le = this.logStore.length; i<le; i++) {
				msg+= this.logStore[i].level + ': ' + this.logStore[i].obj + "\n";
			}
			alert(msg);
		}
		else {
			window.defaultStatus = (level + ': ' + obj);
		}
	}
*/
};


/**
 *	Produces the image map HTML output with the defined areas.
 *	Invokes getMapInnerHTML.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-06 15:10:27
 *	@param	flags	Currently ony 'noscale' used to prevent scaling of coordinates in preview mode.
 *	@return The generated html code.
 */
imgmap.prototype.getMapHTML = function(flags) {
	var html = '<map id="'+this.getMapId()+'" name="'+this.getMapName()+'">' + this.getMapInnerHTML(flags) + this.waterMark + '</map>';
	this.fireEvent('onGetMap', html);
	//alert(html);
	return html;
};


/**
 *	Get the map areas part only of the current imagemap.
 *	@see	#getMapHTML
 *	@author	adam
 *	@param	flags	Currently ony 'noscale' used to prevent scaling of coordinates in preview mode.
 *	@return	The generated map code without the map wrapper.
 */
imgmap.prototype.getMapInnerHTML = function(flags) {
	var html, coords;
	html = '';
	//foreach area properties
	for (var i=0, le = this.areas.length; i<le; i++) {
		if (this.areas[i]) {
			if (this.areas[i].shape && this.areas[i].shape != 'undefined') {
				coords = this.areas[i].lastInput;
				if (flags && flags.match(/noscale/)) {
					//for preview use real coordinates, not scaled
					var cs = coords.split(',');
					for (var j=0, le2 = cs.length; j<le2; j++) {
						cs[j] = Math.round(cs[j] * this.globalscale);
					}
					coords = cs.join(',');
				}
				html+= '<area shape="' + this.areas[i].shape + '"' +
					' alt="' + this.areas[i].aalt + '"' +
					' title="' + this.areas[i].atitle + '"' +
					' coords="' + coords + '"' +
					' href="' +	this.areas[i].ahref + '"' +
					' target="' + this.areas[i].atarget + '" />';
			}
		}
	}
	//alert(html);
	return html;
};


/**
 *	Get the map name of the current imagemap.
 *	If doesnt exist, nor map id, generate a new name based on timestamp.
 *	The most portable solution is to use the same value for id and name.
 *	This also conforms the HTML 5 specification, that says:
 *	"If the id  attribute is also specified, both attributes must have the same value."
 *	@link	http://www.w3.org/html/wg/html5/#the-map-element
 *	@author	adam
 *	@see	#getMapId
 *	@return The name of the map.
 */
imgmap.prototype.getMapName = function() {
	if (this.mapname === '') {
		if (this.mapid !== '') {return this.mapid;}
		var now = new Date();
		this.mapname = 'imgmap' + now.getFullYear() + (now.getMonth()+1) + now.getDate() + now.getHours() + now.getMinutes() + now.getSeconds();
	}
	return this.mapname;
};


/**
 *	Get the map id of the current imagemap.
 *	If doesnt exist, use map name.
 *	@author	adam
 *	@see	#getMapName
 *	@return	The id of the map.
 */
imgmap.prototype.getMapId = function() {
	if (this.mapid === '') {
		this.mapid = this.getMapName();
	}
	return this.mapid;
};


/**
 *	Convert wild shape names to normal ones.
 *	@date	25-12-2008 19:27:06
 *	@param	shape	The name of the shape to convert.
 *	@return	The normalized shape name, rect as default.
 */
imgmap.prototype._normShape = function(shape) {
	if (!shape) {return 'rect';}
	shape = this.trim(shape).toLowerCase();
	if (shape.substring(0, 4) == 'rect') {return 'rect';}
	if (shape.substring(0, 4) == 'circ') {return 'circle';}
	if (shape.substring(0, 4) == 'poly') {return 'poly';}
	return 'rect';
};


/**
 *	Try to normalize coordinates that came from:
 *	1. html textarea
 *	2. user input in the active area's input field
 *	3. from the html source in case of plugins or highlighter
 *	Example of inputs that need to be handled:
 *		035,035 075,062
 *		150,217, 190,257, 150,297,110,257
 *	@author	adam
 *	@param	coords	The coordinates in a string.
 *	@param	shape	The shape of the object (rect, circle, poly, bezier1).
 *	@param	flag	Flags that modify the operation. (fromcircle, frompoly, fromrect, preserve)
 *	@returns The normalized coordinates.
 */
imgmap.prototype._normCoords = function(coords, shape, flag) {
	//function level var declarations
	var i;//generic cycle counter
	var sx;//smallest x
	var sy;//smallest y
	var gx;//greatest x
	var gy;//greatest y
	var temp, le;

	//console.log('normcoords: ' + coords + ' - ' + shape + ' - ' + flag);
	coords = this.trim(coords);
	if (coords === '') {return '';}
	var oldcoords = coords;
	//replace some general junk
	coords = coords.replace(/(\d)(\D)+(\d)/g, "$1,$3");
	coords = coords.replace(/,\D+(\d)/g, ",$1");//cut leading junk
	coords = coords.replace(/,0+(\d)/g, ",$1");//cut leading zeros
	coords = coords.replace(/(\d)(\D)+,/g, "$1,");
	coords = coords.replace(/^\D+(\d)/g, "$1");//cut leading junk
	coords = coords.replace(/^0+(\d)/g, "$1");//cut leading zeros
	coords = coords.replace(/(\d)(\D)+$/g, "$1");//cut trailing junk
	//console.log('>'+coords + ' - ' + shape + ' - ' + flag);
	//now fix other issues
	var parts = coords.split(',');
	if (shape == 'rect') {
		if (flag == 'fromcircle') {
			var r = parts[2];
			parts[0] = parts[0] - r;
			parts[1] = parts[1] - r;
			parts[2] = parseInt(parts[0], 10) + 2 * r;
			parts[3] = parseInt(parts[1], 10) + 2 * r;
		}
		else if (flag == 'frompoly') {
			sx = parseInt(parts[0], 10); gx = parseInt(parts[0], 10);
			sy = parseInt(parts[1], 10); gy = parseInt(parts[1], 10);
			for (i=0, le = parts.length; i<le; i++) {
				if (i % 2 === 0 && parseInt(parts[i], 10) < sx) {
					sx = parseInt(parts[i], 10);}
				if (i % 2 === 1 && parseInt(parts[i], 10) < sy) {
					sy = parseInt(parts[i], 10);}
				if (i % 2 === 0 && parseInt(parts[i], 10) > gx) {
					gx = parseInt(parts[i], 10);}
				if (i % 2 === 1 && parseInt(parts[i], 10) > gy) {
					gy = parseInt(parts[i], 10);}
				//console.log(sx+","+sy+","+gx+","+gy);
			}
			parts[0] = sx; parts[1] = sy;
			parts[2] = gx; parts[3] = gy;
		}
		if (!(parseInt(parts[1], 10) >= 0)) {parts[1] = parts[0];}
		if (!(parseInt(parts[2], 10) >= 0)) {parts[2] = parseInt(parts[0], 10) + 10;}
		if (!(parseInt(parts[3], 10) >= 0)) {parts[3] = parseInt(parts[1], 10) + 10;}
		if (parseInt(parts[0], 10) > parseInt(parts[2], 10)) {
			temp = parts[0];
			parts[0] = parts[2];
			parts[2] = temp;
		}
		if (parseInt(parts[1], 10) > parseInt(parts[3], 10)) {
			temp = parts[1];
			parts[1] = parts[3];
			parts[3] = temp;
		}
		coords = parts[0]+","+parts[1]+","+parts[2]+","+parts[3];
		//console.log(coords);
	}
	else if (shape == 'circle') {
		if (flag == 'fromrect') {
			sx = parseInt(parts[0], 10); gx = parseInt(parts[2], 10);
			sy = parseInt(parts[1], 10); gy = parseInt(parts[3], 10);
			//use smaller side
			parts[2] = (gx - sx < gy - sy) ? gx - sx : gy - sy;
			parts[2] = Math.floor(parts[2] / 2);//radius
			parts[0] = sx + parts[2];
			parts[1] = sy + parts[2];
		}
		else if (flag == 'frompoly') {
			sx = parseInt(parts[0], 10); gx = parseInt(parts[0], 10);
			sy = parseInt(parts[1], 10); gy = parseInt(parts[1], 10);
			for (i=0, le = parts.length; i<le; i++) {
				if (i % 2 === 0 && parseInt(parts[i], 10) < sx) {
					sx = parseInt(parts[i], 10);}
				if (i % 2 === 1 && parseInt(parts[i], 10) < sy) {
					sy = parseInt(parts[i], 10);}
				if (i % 2 === 0 && parseInt(parts[i], 10) > gx) {
					gx = parseInt(parts[i], 10);}
				if (i % 2 === 1 && parseInt(parts[i], 10) > gy) {
					gy = parseInt(parts[i], 10);}
				//console.log(sx+","+sy+","+gx+","+gy);
			}
			//use smaller side
			parts[2] = (gx - sx < gy - sy) ? gx - sx : gy - sy;
			parts[2] = Math.floor(parts[2] / 2);//radius
			parts[0] = sx + parts[2];
			parts[1] = sy + parts[2];
		}
		if (!(parseInt(parts[1], 10) > 0)) {parts[1] = parts[0];}
		if (!(parseInt(parts[2], 10) > 0)) {parts[2] = 10;}
		coords = parts[0]+","+parts[1]+","+parts[2];
	}
	else if (shape == 'poly') {
		if (flag == 'fromrect') {
			parts[4] = parts[2];
			parts[5] = parts[3];
			parts[2] = parts[0];
			parts[6] = parts[4];
			parts[7] = parts[1];
		}
		else if (flag == 'fromcircle') {
			// @ url http://www.pixelwit.com/blog/2007/06/29/basic-circle-drawing-actionscript/
			var centerX = parseInt(parts[0], 10);
			var centerY = parseInt(parts[1], 10);
			var radius  = parseInt(parts[2], 10);
			var j = 0;
			parts[j++] = centerX + radius;
			parts[j++] = centerY;
			var sides = 60;//constant = sides the fake circle will have
			for (i=0; i<=sides; i++) {
				var pointRatio = i/sides;
				var xSteps = Math.cos(pointRatio*2*Math.PI);
				var ySteps = Math.sin(pointRatio*2*Math.PI);
				var pointX = centerX + xSteps * radius;
				var pointY = centerY + ySteps * radius;
				parts[j++] = Math.round(pointX);
				parts[j++] = Math.round(pointY);
			}
			//console.log(parts);
		}
		coords = parts.join(',');
	}
	else if (shape == 'bezier1') {
		coords = parts.join(',');
	}
	if (flag == 'preserve' && oldcoords != coords) {
		//return original and throw error
		//throw "invalid coords";
		return oldcoords;
	}
	return coords;
};


/**
 *	Sets the coordinates according to the given HTML map code or DOM object.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-07 11:47:16
 *	@param	map	DOM object or string of a map you want to apply.
 *	@return	True on success
 */
imgmap.prototype.setMapHTML = function(map) {
	if (this.viewmode === 1) {return;}//exit if preview mode

	this.fireEvent('onSetMap', map);
	//this.log(map);
	//remove all areas
	this.removeAllAreas();
	//console.log(this.areas);

	var oMap;
	if (typeof map == 'string') {
		var oHolder = document.createElement('DIV');
		oHolder.innerHTML = map;
		oMap = oHolder.firstChild;
	}
	else if (typeof map == 'object') {
		oMap = map;
	}
	if (!oMap || oMap.nodeName.toLowerCase() !== 'map') {return false;}
	this.mapname = oMap.name;
	this.mapid   = oMap.id;
	var newareas = oMap.getElementsByTagName('area');
	var shape, coords, href, alt, title, target, id;
	for (var i=0, le = newareas.length; i<le; i++) {
		shape = coords = href = alt = title = target = '';

		id = this.addNewArea();//btw id == this.currentid, just this form is a bit clearer

		shape = this._normShape(newareas[i].getAttribute('shape', 2));

		this.initArea(id, shape);

		if (newareas[i].getAttribute('coords', 2)) {
			//normalize coords
			coords = this._normCoords(newareas[i].getAttribute('coords', 2), shape);
			this.areas[id].lastInput = coords;
			//for area this one will be set in recalculate
		}

		href = newareas[i].getAttribute('href', 2);
		// FCKeditor stored url to prevent mangling from the browser.
		var sSavedUrl = newareas[i].getAttribute( '_fcksavedurl' );
		if (sSavedUrl) {
			href = sSavedUrl;
		}
		if (href) {
			this.areas[id].ahref = href;
		}

		alt = newareas[i].getAttribute('alt');
		if (alt) {
			this.areas[id].aalt = alt;
		}

		title = newareas[i].getAttribute('title');
		if (!title) {title = alt;}
		if (title) {
			this.areas[id].atitle = title;
		}

		target = newareas[i].getAttribute('target');
		if (target) {target = target.toLowerCase();}
//		if (target == '') target = '_self';
		this.areas[id].atarget = target;

		this._recalculate(id, coords);//contains repaint
		this.relaxArea(id);

		this.fireEvent('onAreaChanged', this.areas[id]);

	}//end for areas
	this.fireEvent('onHtmlChanged', this.getMapHTML());
	return true;
};


/**
 *	Preview image with imagemap applied.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-06 14:51:01
 *	@url	http://www.quirksmode.org/bugreports/archives/2005/03/Usemap_attribute_wrongly_case_sensitive.html
 *	@return	False on error, 0 when switched to edit mode, 1 when switched to preview mode
 */
imgmap.prototype.togglePreview = function() {
	var i, le;//generic cycle counter

	if (!this.pic) {return false;}//exit if pic is undefined

	//dynamically create preview container
	if (!this.preview) {
		this.preview = document.createElement('DIV');
		this.preview.style.display = 'none';
		this.pic_container.appendChild(this.preview);
	}

	if (this.viewmode === 0) {
		//hide canvas elements and labels
		for (i = 0, le = this.areas.length; i < le; i++) {
			if (this.areas[i]) {
				this.areas[i].style.display = 'none';
				if (this.areas[i].label) {this.areas[i].label.style.display = 'none';}
			}
		}
		//activate image map
		this.preview.innerHTML = this.getMapHTML('noscale');
		this.pic.setAttribute('border', '0', 0);
		this.pic.setAttribute('usemap', '#' + this.mapname, 0);
		this.pic.style.cursor  = 'auto';
		this.viewmode = 1;
		this.statusMessage(this.strings.PREVIEW_MODE);
	}
	else {
		//show canvas elements
		for (i = 0, le = this.areas.length; i < le; i++) {
			if (this.areas[i]) {
				this.areas[i].style.display = '';
				if (this.areas[i].label && this.config.label) {this.areas[i].label.style.display = '';}
			}
		}
		//clear image map
		this.preview.innerHTML = '';
		this.pic.style.cursor  = this.config.cursor_default;
		this.pic.removeAttribute('usemap', 0);
		this.viewmode = 0;
		this.statusMessage(this.strings.DESIGN_MODE);
		this.is_drawing = 0;
	}
	this.fireEvent('onModeChanged', this.viewmode);
	return this.viewmode;
};


/**
 *	Adds a new area. It will later become a canvas.
 *	GUI should use the onAddArea callback to act accordingly.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-06 16:49:25
 *	@see	#initArea
 */
imgmap.prototype.addNewArea = function() {
		if (this.viewmode === 1) {return;}//exit if preview mode
		var lastarea = this._getLastArea();
		var id = (lastarea) ? lastarea.aid + 1 : 0;
		//alert(id);

		//insert new possibly? unknown area (will be initialized at mousedown)
		this.areas[id] = document.createElement('DIV');
		this.areas[id].id        = this.mapname + 'area' + id;
		this.areas[id].aid       = id;
		this.areas[id].shape     = "undefined";

		this.currentid = id;
		this.fireEvent('onAddArea', id);
		return id;
};


/**
 *	Initialize a new area.
 *	Create the canvas, initialize it.
 *	Reset area parameters.
 *	@param	id	The id of the area (already existing with undefined shape)
 *	@param	shape	The shape the area will have (rect, circle, poly, bezier1)
 */
imgmap.prototype.initArea = function(id, shape) {
	if (!this.areas[id]) {return false;}//if all was erased, return
	//remove preinited dummy div or already placed canvas
	if (this.areas[id].parentNode) {this.areas[id].parentNode.removeChild(this.areas[id]);}
	if (this.areas[id].label) {this.areas[id].label.parentNode.removeChild(this.areas[id].label);}
	this.areas[id] = null;
	//create CANVAS node
	this.areas[id] = document.createElement('CANVAS');
	this.pic_container.appendChild(this.areas[id]);
	this.pic_container.style.position = 'relative';
	//alert('init' + typeof G_vmlCanvasManager);
	if (typeof G_vmlCanvasManager != "undefined") {
		//override CANVAS with VML object
		this.areas[id] = G_vmlCanvasManager.initElement(this.areas[id]);
		//this.areas[id] = this.pic.parentNode.lastChild;
	}
	this.areas[id].id        = this.mapname + 'area' + id;
	this.areas[id].aid       = id;
	this.areas[id].shape     = shape;
	this.areas[id].ahref     = '';
	this.areas[id].atitle    = '';
	this.areas[id].aalt      = '';
	this.areas[id].atarget   = ''; // '_self';
	this.areas[id].style.position = 'absolute';
	this.areas[id].style.top      = this.pic.offsetTop  + 'px';
	this.areas[id].style.left     = this.pic.offsetLeft + 'px';
	this._setopacity(this.areas[id], this.config.CL_DRAW_BG, this.config.draw_opacity);
	//hook event handlers
	this.areas[id].ondblclick  = this.area_dblclick.bind(this);
	this.areas[id].onmousedown = this.area_mousedown.bind(this);
	this.areas[id].onmouseup   = this.area_mouseup.bind(this);
	this.areas[id].onmousemove = this.area_mousemove.bind(this);
	this.areas[id].onmouseover = this.area_mouseover.bind(this);
	this.areas[id].onmouseout  = this.area_mouseout.bind(this);
	//initialize memory object
	this.memory[id] = {};
	this.memory[id].downx   = 0;
	this.memory[id].downy   = 0;
	this.memory[id].left    = 0;
	this.memory[id].top     = 0;
	this.memory[id].width   = 0;
	this.memory[id].height  = 0;
	this.memory[id].xpoints = [];
	this.memory[id].ypoints = [];
	//create label node
	this.areas[id].label = document.createElement('DIV');
	this.pic_container.appendChild(this.areas[id].label);
	this.areas[id].label.className      = this.config.label_class;
	this.assignCSS(this.areas[id].label,  this.config.label_style);
	this.areas[id].label.style.position = 'absolute';
};


/**
 *	Resets area border and opacity to a normal state after drawing.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	15-02-2007 22:07:28
 *	@param	id	The id of the area.
 *	@see	#relaxAllAreas
 */
imgmap.prototype.relaxArea = function(id) {
	if (!this.areas[id]) {return;}
	this.fireEvent('onRelaxArea', id);
	this._setBorder(id, 'NORM');
	this._setopacity(this.areas[id], this.config.CL_NORM_BG, this.config.norm_opacity);
};


/**
 *	Resets area border and opacity of all areas.
 *	Calls relaxArea on each of them.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	23-04-2007 23:31:09
 *	@see	#relaxArea
 */
imgmap.prototype.relaxAllAreas = function() {
	for (var i=0, le = this.areas.length; i<le; i++) {
		if (this.areas[i]) {
			this.relaxArea(i);
		}
	}
};


/**
 *	Set border of a given area according to style flag.
 *	Possible values of style: NORM, HIGHLIGHT, DRAW.
 *	Non-rectangle shapes wont get a border if config.bounding_box is false.
 *	@date	26-12-2008 22:34:41
 *	@param	id	The id of the area to set the border on.
 *	@param	style	Coloring style (NORM, HIGHLIGHT, DRAW), see relevant colors in config.
 *	@since	2.1
 */
imgmap.prototype._setBorder = function(id, style) {
	if (this.areas[id].shape == 'rect' || this.config.bounding_box) {
		this.areas[id].style.borderWidth = '1px';
		this.areas[id].style.borderStyle = (style == 'DRAW' ? 'dotted' : 'solid');
		this.areas[id].style.borderColor = this.config['CL_' + style + '_' + (this.areas[id].shape == 'rect' ? 'SHAPE' : 'BOX')];
	}
	else {
		//clear border
		this.areas[id].style.border = '';
	}
};


/**
 *	Set opacity of area to the given percentage, as well as set the background color.
 *	If percentage contains a dash(-), the setting of the opacity will be gradual.
 *	@param	area	The area object.
 *	@param	bgcolor	New background color
 *	@param	pct		Percentage of the opacity.
 */
imgmap.prototype._setopacity = function(area, bgcolor, pct) {
	if (bgcolor) {area.style.backgroundColor = bgcolor;}
	if (pct && typeof pct == 'string' && pct.match(/^\d*\-\d+$/)) {
		//gradual fade
		var parts = pct.split('-');
		if (typeof parts[0] != 'undefined') {
			//set initial opacity
			parts[0] = parseInt(parts[0], 10);
			this._setopacity(area, bgcolor, parts[0]);
		}
		if (typeof parts[1] != 'undefined') {
			parts[1] = parseInt(parts[1], 10);
			var curr = this._getopacity(area);
			//this.log('curr: '+curr);
			var _this = this;
			var diff = Math.round(parts[1] - curr);
			if (diff > 5) {
				window.setTimeout(function () {_this._setopacity(area, null, '-'+parts[1]);}, 20);
				pct = 1*curr + 5;
			}
			else if (diff < -3) {
				window.setTimeout(function () {_this._setopacity(area, null, '-'+parts[1]);}, 20);
				pct = 1*curr - 3;
			}
			else {
				//final set
				pct = parts[1];
			}
		}
	}
	if (!isNaN(pct)) {
		pct = Math.round(parseInt(pct, 10));
		//this.log('set ('+area.aid+'): ' + pct, 1);
		area.style.opacity = pct / 100;
		area.style.filter  = 'alpha(opacity='+pct+')';
	}
};


/**
 *	Get the currently set opacity of a given area.
 *	@author	adam
 *	@param	area	The area (canvas) you want to get opacity info from.
 *	@return	Opacity value in a range of 0-100.
 */
imgmap.prototype._getopacity = function(area) {
	if (area.style.opacity <= 1) {
		return area.style.opacity * 100;
	}
	if (area.style.filter) {
		//alpha(opacity=NaN)
		return parseInt(area.style.filter.replace(/alpha\(opacity\=([^\)]*)\)/ig, "$1"), 10);
	}
	return 100;//default opacity
};


/**
 *	Removes the area marked by id.
 *	removeAllAreas will indicate a mass flag so that the output HTML will only be updated at
 *	the end of the operation.
 *	Callback will call the GUI code to remove GUI elements.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	11-02-2007 20:40:58
 *	@param	id	The id of the area to remove.
 *	@param	mass	Flag to indicate skipping the call of onHtmlChanged callback
 *	@see	#removeAllAreas
 */
imgmap.prototype.removeArea = function(id, mass) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	if (id === null || typeof id == "undefined") {return;}//exit if no id given

	try {
		//remove area and label
		//explicitly set some values to null to avoid IE circular reference memleak
		this.areas[id].label.parentNode.removeChild(this.areas[id].label);
		this.areas[id].parentNode.removeChild(this.areas[id]);
		this.areas[id].label.className = null;
		//this.areas[id].label.style = null;
		//console.log(this.areas[id].label);
		this.areas[id].label       = null;
		this.areas[id].onmouseover = null;
		this.areas[id].onmouseout  = null;
		this.areas[id].onmouseup   = null;
		this.areas[id].onmousedown = null;
		this.areas[id].onmousemove = null;
//		console.log(this.areas[id].label);

	}
	catch (err) {
		//alert('noparent');
	}
	this.areas[id] = null;
	this.fireEvent('onRemoveArea', id);
	//update grand html
	if (!mass) {this.fireEvent('onHtmlChanged', this.getMapHTML());}
};


/**
 *	Removes all areas.
 *	Will call removeArea on all areas.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-07 11:55:34
 *	@see	#removeArea
 */
imgmap.prototype.removeAllAreas = function() {
	for (var i = 0, le = this.areas.length; i < le; i++) {
		if (this.areas[i]) {
			this.removeArea(i, true);
		}
	}
	//only call this at the end, use mass param above to avoid calling it in process
	this.fireEvent('onHtmlChanged', this.getMapHTML());
};


/**
 *	Scales all areas.
 *	Will store scale parameter in globalscale property.
 *	This is needed to know how to draw new areas on an already scaled canvas.
 *	@author	adam
 *	@date	02-11-2008 14:13:14
 *	@param scale	Scale factor (1-original, 0.5-half, 2-double, etc.)
 */
imgmap.prototype.scaleAllAreas = function(scale) {
	var rscale = 1;//relative scale
	try {
		rscale = scale / this.globalscale;
	}
	catch (err) {
		this.log("Invalid (global)scale", 1);
	}
	//console.log('gscale: '+this.globalscale);
	//console.log('scale: '+scale);
	//console.log('rscale: '+rscale);

	this.globalscale = scale;
	for (var i = 0, le = this.areas.length; i < le; i++) {
		if (this.areas[i] && this.areas[i].shape != 'undefined') {
			this.scaleArea(i, rscale);
		}
	}
};


/**
 *	Scales one area.
 *	@author	adam
 *	@date	02-11-2008 14:13:14
 *	@param rscale	Relative scale factor (1-keep, 0.5-half, 2-double, etc.)
 */
imgmap.prototype.scaleArea = function(id, rscale) {

	//set position and new dimensions
	this.areas[id].style.top  = parseInt(this.areas[id].style.top, 10) * rscale + 'px';
	this.areas[id].style.left = parseInt(this.areas[id].style.left, 10) * rscale + 'px';
	this.setAreaSize(id, this.areas[id].width * rscale, this.areas[id].height * rscale);

	//handle polygon/bezier coordinates scaling
	if (this.areas[id].shape == 'poly' || this.areas[id].shape == 'bezier1') {
		for (var i=0, le = this.areas[id].xpoints.length; i<le; i++) {
			this.areas[id].xpoints[i]*= rscale;
			this.areas[id].ypoints[i]*= rscale;
		}
	}

	this._repaint(this.areas[id], this.config.CL_NORM_SHAPE);
	this._updatecoords(id);
};


/**
 *	Put label in the top left corner according to label config.
 *	By default it will contain the number of the area (area.aid)
 *	@param	id	The id of the area to add label to.
 */
imgmap.prototype._putlabel = function(id) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	if (!this.areas[id].label) {return;}//not yet inited
	try {
		if (!this.config.label) {
			this.areas[id].label.innerHTML     = '';
			this.areas[id].label.style.display = 'none';
		}
		else {
			this.areas[id].label.style.display = '';
			var label = this.config.label;
			label = label.replace(/%n/g, String(id));
			label = label.replace(/%c/g, String(this.areas[id].lastInput));
			label = label.replace(/%h/g, String(this.areas[id].ahref));
			label = label.replace(/%a/g, String(this.areas[id].aalt));
			label = label.replace(/%t/g, String(this.areas[id].atitle));
			this.areas[id].label.innerHTML = label;
		}
		//align to the top left corner
		this.areas[id].label.style.top  = this.areas[id].style.top;
		this.areas[id].label.style.left = this.areas[id].style.left;
	}
	catch (err) {
		this.log("Error putting label", 1);
	}
};


/**
 *	Set area title and alt (for IE) according to the hint configuration.
 *	This will show up in the usual yellow box when you hover over with the mouse.
 *	@param	id	The id of the area to set hint at.
 */
imgmap.prototype._puthint = function(id) {
	try {
		if (!this.config.hint) {
			this.areas[id].title = '';
			this.areas[id].alt   = '';
		}
		else {
			var hint = this.config.hint;
			hint = hint.replace(/%n/g, String(id));
			hint = hint.replace(/%c/g, String(this.areas[id].lastInput));
			hint = hint.replace(/%h/g, String(this.areas[id].ahref));
			hint = hint.replace(/%a/g, String(this.areas[id].aalt));
			hint = hint.replace(/%t/g, String(this.areas[id].atitle));
			this.areas[id].title = hint;
			this.areas[id].alt   = hint;
		}
	}
	catch (err) {
		this.log("Error putting hint", 1);
	}
};


/**
 *	Will call repaint on all areas.
 *	Useful when you change labeling or hint config on the GUI.
 *	@see #_repaint
 */
imgmap.prototype._repaintAll = function() {
	for (var i=0, le = this.areas.length; i<le; i++) {
		if (this.areas[i]) {
			this._repaint(this.areas[i], this.config.CL_NORM_SHAPE);
		}
	}
};


/**
 *	Repaints the actual canvas content.
 *	This is the only canvas drawing magic that is happening.
 *	In fact rectangles will not have any canvas content, just a normal css border.
 *	After repainting the canvas, it will call putlabel and puthint methods.
 *	@param	area	The area object.
 *	@param	color	Color of the line to draw on the canvas.
 *	@param	x	Only used for polygons/beziers as the newest control point x.
 *	@param	y	Only used for polygons/beziers as the newest control point y.
 */
imgmap.prototype._repaint = function(area, color, x, y) {
	var ctx;//canvas context
	var width, height, left, top;//canvas properties
	var i, le;//loop counter
	if (area.shape == 'circle') {
		width  = parseInt(area.style.width, 10);
		var radius = Math.floor(width/2) - 1;
		if (radius<0)
			radius=0;

		//get canvas context
		//alert(area.tagName);
		ctx = area.getContext("2d");
		//clear canvas
		ctx.clearRect(0, 0, width, width);
		//draw circle
		ctx.beginPath();
		ctx.strokeStyle = color;
		ctx.arc(radius, radius, radius, 0, Math.PI*2, 0);
		ctx.stroke();
		ctx.closePath();
		//draw center
		ctx.strokeStyle = this.config.CL_KNOB;
		ctx.strokeRect(radius, radius, 1, 1);
		//put label
		this._putlabel(area.aid);
		this._puthint(area.aid);
	}
	else if (area.shape == 'rect') {
		//put label
		this._putlabel(area.aid);
		this._puthint(area.aid);
	}
	else if (area.shape == 'poly') {
		width  =  parseInt(area.style.width, 10);
		height =  parseInt(area.style.height, 10);
		left   =  parseInt(area.style.left, 10);
		top    =  parseInt(area.style.top, 10);
		if (area.xpoints) {
			//get canvas context
			ctx = area.getContext("2d");
			//clear canvas
			ctx.clearRect(0, 0, width, height);
			//draw polygon
			ctx.beginPath();
			ctx.strokeStyle = color;
			ctx.moveTo(area.xpoints[0] - left, area.ypoints[0] - top);
			for (i = 1, le = area.xpoints.length; i < le; i++) {
				ctx.lineTo(area.xpoints[i] - left , area.ypoints[i] - top);
			}
			if (this.is_drawing == this.DM_POLYGON_DRAW || this.is_drawing == this.DM_POLYGON_LASTDRAW) {
				//only draw to the current position if not moving
				ctx.lineTo(x - left - 5 , y - top - 5);
			}
			ctx.lineTo(area.xpoints[0] - left , area.ypoints[0] - top);
			ctx.stroke();
			ctx.closePath();
		}
		//put label
		this._putlabel(area.aid);
		this._puthint(area.aid);
	}
	else if (area.shape == 'bezier1') {
		width  =  parseInt(area.style.width, 10);
		height =  parseInt(area.style.height, 10);
		left   =  parseInt(area.style.left, 10);
		top    =  parseInt(area.style.top, 10);
		if (area.xpoints) {
			//get canvas context
			ctx = area.getContext("2d");
			//clear canvas
			ctx.clearRect(0, 0, width, height);
			//draw bezier1 (every second point is control point)
			ctx.beginPath();
			ctx.strokeStyle = color;
			//move to the beginning position
			ctx.moveTo(area.xpoints[0] - left, area.ypoints[0] - top);
			//draw previous points - use every second point only
			for (i = 2, le = area.xpoints.length; i < le; i+= 2) {
				ctx.quadraticCurveTo(area.xpoints[i-1] - left, area.ypoints[i-1] - top, area.xpoints[i] - left, area.ypoints[i] - top);
			}
			if (this.is_drawing == this.DM_BEZIER_DRAW || this.is_drawing == this.DM_BEZIER_LASTDRAW) {
				//only draw to the current position if not moving
				if (area.xpoints.length % 2 === 0 && area.xpoints.length > 1) {
					//drawing point - draw a curve to it using the previous control point
					ctx.quadraticCurveTo(area.xpoints[area.xpoints.length - 1] - left - 5 , area.ypoints[area.ypoints.length - 1] - top - 5, x - left - 5 , y - top - 5);
				}
				else {
					//control point - simply draw a line to it
					ctx.lineTo(x - left - 5 , y - top - 5);
				}
			}
			//close area by drawing a line to the first point
			ctx.lineTo(area.xpoints[0] - left , area.ypoints[0] - top);
			ctx.stroke();
			ctx.closePath();
		}
		//put label
		this._putlabel(area.aid);
		this._puthint(area.aid);
	}
};


/**
 *	Updates Area coordinates.
 *	Called when needed, eg. on mousemove, mousedown.
 *	Also updates html container value (thru hook).
 *	Calls callback onAreaChanged and onHtmlChanged so that GUI can follow.
 *	This is an important hook to your GUI.
 *	Uses globalscale to scale real coordinates to area coordinates.
 *	@date	2006.10.24. 22:39:27
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	id	The id of the area.
 */
imgmap.prototype._updatecoords = function(id) {

	var left   = Math.round(parseInt(this.areas[id].style.left, 10) / this.globalscale);
	var top    = Math.round(parseInt(this.areas[id].style.top, 10) / this.globalscale);
	var height = Math.round(parseInt(this.areas[id].style.height, 10) / this.globalscale);
	var width  = Math.round(parseInt(this.areas[id].style.width, 10) / this.globalscale);

	var value = '';
	if (this.areas[id].shape == 'rect') {
		value = left + ',' + top + ',' + (left + width) + ',' + (top + height);
		this.areas[id].lastInput = value;
	}
	else if (this.areas[id].shape == 'circle') {
		var radius = Math.floor(width/2) - 1;
		value = (left + radius) + ',' +	(top + radius) + ',' + radius;
		this.areas[id].lastInput = value;
	}
	else if (this.areas[id].shape == 'poly' || this.areas[id].shape == 'bezier1') {
		if (this.areas[id].xpoints) {
			for (var i=0, le = this.areas[id].xpoints.length; i<le; i++) {
				value+= Math.round(this.areas[id].xpoints[i] / this.globalscale) + ',' +
						Math.round(this.areas[id].ypoints[i] / this.globalscale) + ',';
			}
			value = value.substring(0, value.length - 1);
		}
		this.areas[id].lastInput = value;
	}

	this.fireEvent('onAreaChanged', this.areas[id]);
	this.fireEvent('onHtmlChanged', this.getMapHTML());
};


/**
 *	Updates the visual representation of the area with the given id according
 *	to the new coordinates that typically come from an input on the GUI.
 *	Uses globalscale to scale area coordinates to real coordinates.
 *	@date	2006.10.24. 22:46:55
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@param	id	The id of the area.
 *	@param	coords	The new coords, they will be normalized.
 */
imgmap.prototype._recalculate = function(id, coords) {
	try {
		if (coords) {
			coords = this._normCoords(coords, this.areas[id].shape, 'preserve');
		}
		else {
			coords = this.areas[id].lastInput || '' ;
		}

		var parts   = coords.split(',');
		if (this.areas[id].shape == 'rect') {
			if (parts.length != 4 ||
				parseInt(parts[0], 10) > parseInt(parts[2], 10) ||
				parseInt(parts[1], 10) > parseInt(parts[3], 10)) {throw "invalid coords";}
			this.areas[id].style.left   = this.globalscale * (this.pic.offsetLeft + parseInt(parts[0], 10)) + 'px';
			this.areas[id].style.top    = this.globalscale * (this.pic.offsetTop  + parseInt(parts[1], 10)) + 'px';
			this.setAreaSize(id, this.globalscale * (parts[2] - parts[0]), this.globalscale  * (parts[3] - parts[1]));
			this._repaint(this.areas[id], this.config.CL_NORM_SHAPE);
		}
		else if (this.areas[id].shape == 'circle') {
			if (parts.length != 3 ||
				parseInt(parts[2], 10) < 0) {throw "invalid coords";}
			var width = 2 * (parts[2]);
			//alert(parts[2]);
			//alert(width);
			this.setAreaSize(id, this.globalscale * width, this.globalscale * width);
			this.areas[id].style.left   = this.globalscale * (this.pic.offsetLeft + parseInt(parts[0], 10) - width/2) + 'px';
			this.areas[id].style.top    = this.globalscale * (this.pic.offsetTop  + parseInt(parts[1], 10) - width/2) + 'px';
			this._repaint(this.areas[id], this.config.CL_NORM_SHAPE);
		}
		else if (this.areas[id].shape == 'poly' || this.areas[id].shape == 'bezier1') {
			if (parts.length < 2) {throw "invalid coords";}
			this.areas[id].xpoints = [];
			this.areas[id].ypoints = [];
			for (var i=0, le = parts.length; i<le; i+=2) {
				this.areas[id].xpoints[this.areas[id].xpoints.length]  = this.globalscale * (this.pic.offsetLeft + parseInt(parts[i], 10));
				this.areas[id].ypoints[this.areas[id].ypoints.length]  = this.globalscale * (this.pic.offsetTop  + parseInt(parts[i+1], 10));
				this._polygongrow(this.areas[id], this.globalscale * parts[i], this.globalscale * parts[i+1]);
			}
			this._polygonshrink(this.areas[id]);//includes repaint
		}
	}
	catch (err) {
		var msg = (err.message) ? err.message : 'error calculating coordinates';
		this.log(msg, 1);
		this.statusMessage(this.strings.ERR_INVALID_COORDS);
		if (this.areas[id].lastInput) {
			this.fireEvent('onAreaChanged', this.areas[id]);
		}
		this._repaint(this.areas[id], this.config.CL_NORM_SHAPE);
		return;
	}
	//on success update lastInput
	this.areas[id].lastInput = coords;
};


/**
 *	Grow polygon area to be able to contain the given new coordinates.
 *	@author	adam
 *	@param	area	The area to grow.
 *	@param	newx	The new coordinate x.
 *	@param	newy	The new coordinate y.
 *	@see	#_polygonshrink
 */
imgmap.prototype._polygongrow = function(area, newx, newy) {
	//this.log('pgrow');
	var xdiff = newx - parseInt(area.style.left, 10);
	var ydiff = newy - parseInt(area.style.top , 10);
	var pad   = 0;//padding on the edges
	var pad2  = 0;//twice the padding

	if (newx < parseInt(area.style.left, 10)) {
		area.style.left   = (newx - pad) + 'px';
		this.setAreaSize(area.aid, parseInt(area.style.width, 10) + Math.abs(xdiff) + pad2, null);
	}
	else if (newx > parseInt(area.style.left, 10) + parseInt(area.style.width, 10)) {
		this.setAreaSize(area.aid, newx - parseInt(area.style.left, 10) + pad2, null);
	}
	if (newy < parseInt(area.style.top, 10)) {
		area.style.top    = (newy - pad) + 'px';
		this.setAreaSize(area.aid, null, parseInt(area.style.height, 10) + Math.abs(ydiff) + pad2);
	}
	else if (newy > parseInt(area.style.top, 10) + parseInt(area.style.height, 10)) {
		this.setAreaSize(area.aid, null, newy - parseInt(area.style.top, 10) + pad2);
	}
};


/**
 *	Shrink the polygon bounding area to the necessary size, by first reducing it
 *	to the minimum, and then gradually growing it.
 *	We need this because while we were drawing the polygon, it might have expanded
 *	the canvas more than needed.
 *	Will repaint the area.
 *	@author	adam
 *	@param	area	The area to shrink.
 *	@see	#_polygongrow
 */
imgmap.prototype._polygonshrink = function(area) {
	//this.log('pshrink');
	area.style.left = (area.xpoints[0]) + 'px';
	area.style.top  = (area.ypoints[0]) + 'px';
	this.setAreaSize(area.aid, 0, 0);
	for (var i=0, le = area.xpoints.length; i<le; i++) {
		this._polygongrow(area, area.xpoints[i], area.ypoints[i]);
	}
	this._repaint(area, this.config.CL_NORM_SHAPE);
};


/**
 *	EVENT HANDLER: Handles mousemove on the image.
 *	This is the main drawing routine.
 *	Depending on the current shape, will draw the rect/circle/poly to the new position.
 *	@param	e	The event object.
 */
imgmap.prototype.img_mousemove = function(e) {
	//function level var declarations
	var x;
	var y;
	var xdiff;
	var ydiff;
	var diff;

	if (this.viewmode === 1) {return;}//exit if preview mode
	//event.x is relative to parent element, but page.x is NOT
	//pos coordinates are the same absolute coords, offset coords are relative to parent
	var pos = this._getPos(this.pic);
	// 3.3.3 fix
	x = (this.isMSIE) ? (window.event.x - this.pic.offsetLeft) : (e.clientX - pos.x);
	y = (this.isMSIE) ? (window.event.y - this.pic.offsetTop)  : (e.clientY - pos.y);
	x = x + this.pic_container.scrollLeft;
	y = y + this.pic_container.scrollTop;

	//exit if outside image
	if (x<0 || y<0 || x>this.pic.width || y>this.pic.height) {return;}

	//old dimensions that need to be updated in this function
	if (this.memory[this.currentid]) {
		var top    = this.memory[this.currentid].top;
		var left   = this.memory[this.currentid].left;
		var height = this.memory[this.currentid].height;
		var width  = this.memory[this.currentid].width;
	}

	// Handle shift state for Safari
	// Safari doesn't generate keyboard events for modifiers: http://bugs.webkit.org/show_bug.cgi?id=11696
	if (this.isSafari) {
		if (e.shiftKey) {
			if (this.is_drawing == this.DM_RECTANGLE_DRAW) {
				this.is_drawing = this.DM_SQUARE_DRAW;
				this.statusMessage(this.strings.SQUARE2_DRAW);
			}
		}
		else {
			if (this.is_drawing == this.DM_SQUARE_DRAW && this.areas[this.currentid].shape == 'rect') {
				//not for circle!
				this.is_drawing = this.DM_RECTANGLE_DRAW;
				this.statusMessage(this.strings.RECTANGLE_DRAW);
			}
		}
	}


	if (this.is_drawing == this.DM_RECTANGLE_DRAW) {
		//rectangle mode
		this.fireEvent('onDrawArea', this.currentid);
		xdiff = x - this.memory[this.currentid].downx;
		ydiff = y - this.memory[this.currentid].downy;
		//alert(xdiff);
		this.setAreaSize(this.currentid, Math.abs(xdiff), Math.abs(ydiff));
		if (xdiff < 0) {
			this.areas[this.currentid].style.left = (x + 1) + 'px';
		}
		if (ydiff < 0) {
			this.areas[this.currentid].style.top  = (y + 1) + 'px';
		}
	}
	else if (this.is_drawing == this.DM_SQUARE_DRAW) {
		//square mode - align to shorter side
		this.fireEvent('onDrawArea', this.currentid);
		xdiff = x - this.memory[this.currentid].downx;
		ydiff = y - this.memory[this.currentid].downy;
		if (Math.abs(xdiff) < Math.abs(ydiff)) {
			diff = Math.abs(parseInt(xdiff, 10));
		}
		else {
			diff = Math.abs(parseInt(ydiff, 10));
		}
		//alert(xdiff);
		this.setAreaSize(this.currentid, diff, diff);
		if (xdiff < 0) {
			this.areas[this.currentid].style.left = (this.memory[this.currentid].downx + diff*-1) + 'px';
		}
		if (ydiff < 0) {
			this.areas[this.currentid].style.top = (this.memory[this.currentid].downy + diff*-1 + 1) + 'px';
		}
	}
	else if (this.is_drawing == this.DM_POLYGON_DRAW || this.is_drawing == this.DM_BEZIER_DRAW) {
		//polygon or bezier mode
		this.fireEvent('onDrawArea', this.currentid);
		this._polygongrow(this.areas[this.currentid], x, y);
	}
	else if (this.is_drawing == this.DM_RECTANGLE_MOVE || this.is_drawing == this.DM_SQUARE_MOVE) {
		this.fireEvent('onMoveArea', this.currentid);
		x = x - this.memory[this.currentid].rdownx;
		y = y - this.memory[this.currentid].rdowny;
		if (x + width > this.pic.width || y + height > this.pic.height) {return;}
		if (x < 0 || y < 0) {return;}
		//this.log(x + ' - '+width+ '+'+this.memory[this.currentid].rdownx +'='+xdiff );
		this.areas[this.currentid].style.left = x + 1 + 'px';
		this.areas[this.currentid].style.top  = y + 1 + 'px';
	}
	else if (this.is_drawing == this.DM_POLYGON_MOVE || this.is_drawing == this.DM_BEZIER_MOVE) {
		this.fireEvent('onMoveArea', this.currentid);
		x = x - this.memory[this.currentid].rdownx;
		y = y - this.memory[this.currentid].rdowny;
		if (x + width > this.pic.width || y + height > this.pic.height) {return;}
		if (x < 0 || y < 0) {return;}
		xdiff = x - left;
		ydiff = y - top;
		if (this.areas[this.currentid].xpoints) {
			for (var i=0, le = this.areas[this.currentid].xpoints.length; i<le; i++) {
				this.areas[this.currentid].xpoints[i] = this.memory[this.currentid].xpoints[i] + xdiff;
				this.areas[this.currentid].ypoints[i] = this.memory[this.currentid].ypoints[i] + ydiff;
			}
		}
		this.areas[this.currentid].style.left = x + 'px';
		this.areas[this.currentid].style.top  = y + 'px';
	}
	else if (this.is_drawing == this.DM_SQUARE_RESIZE_LEFT) {
		this.fireEvent('onResizeArea', this.currentid);
		diff = x - left;
		//alert(diff);
		if ((width  + (-1 * diff)) > 0) {
			//real resize left
			this.areas[this.currentid].style.left   = x + 1 + 'px';
			this.areas[this.currentid].style.top    = (top    + (diff/2)) + 'px';
			this.setAreaSize(this.currentid, parseInt(width  + (-1 * diff), 10), parseInt(height + (-1 * diff), 10));
		}
		else {
			//jump to another state
			this.memory[this.currentid].width  = 0;
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].left   = x;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_SQUARE_RESIZE_RIGHT;
		}
	}
	else if (this.is_drawing == this.DM_SQUARE_RESIZE_RIGHT) {
		this.fireEvent('onResizeArea', this.currentid);
		diff = x - left - width;
		if ((width  + (diff)) - 1 > 0) {
			//real resize right
			this.areas[this.currentid].style.top    = (top    + (-1* diff/2)) + 'px';
			this.setAreaSize(this.currentid, (width  + (diff)) - 1, (height + (diff)));
		}
		else {
			//jump to another state
			this.memory[this.currentid].width  = 0;
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].left   = x;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_SQUARE_RESIZE_LEFT;
		}
	}
	else if (this.is_drawing == this.DM_SQUARE_RESIZE_TOP) {
		this.fireEvent('onResizeArea', this.currentid);
		diff = y - top;
		if ((width  + (-1 * diff)) > 0) {
			//real resize top
			this.areas[this.currentid].style.top    = y + 1 + 'px';
			this.areas[this.currentid].style.left   = (left   + (diff/2)) + 'px';
			this.setAreaSize(this.currentid, (width  + (-1 * diff)), (height + (-1 * diff)));
		}
		else {
			//jump to another state
			this.memory[this.currentid].width  = 0;
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].left   = x;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_SQUARE_RESIZE_BOTTOM;
		}
	}
	else if (this.is_drawing == this.DM_SQUARE_RESIZE_BOTTOM) {
		this.fireEvent('onResizeArea', this.currentid);
		diff = y - top - height;
		if ((width  + (diff)) - 1 > 0) {
			//real resize bottom
			this.areas[this.currentid].style.left   = (left   + (-1* diff/2)) + 'px';
			this.setAreaSize(this.currentid, (width  + (diff)) - 1 , (height + (diff)) - 1);
		}
		else {
			//jump to another state
			this.memory[this.currentid].width  = 0;
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].left   = x;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_SQUARE_RESIZE_TOP;
		}
	}
	else if (this.is_drawing == this.DM_RECTANGLE_RESIZE_LEFT) {
		this.fireEvent('onResizeArea', this.currentid);
		xdiff = x - left;
		if (width + (-1 * xdiff) > 0) {
			//real resize left
			this.areas[this.currentid].style.left = x + 1 + 'px';
			this.setAreaSize(this.currentid, width + (-1 * xdiff), null);
		}
		else {
			//jump to another state
			this.memory[this.currentid].width = 0;
			this.memory[this.currentid].left  = x;
			this.is_drawing = this.DM_RECTANGLE_RESIZE_RIGHT;
		}
	}
	else if (this.is_drawing == this.DM_RECTANGLE_RESIZE_RIGHT) {
		this.fireEvent('onResizeArea', this.currentid);
		xdiff = x - left - width;
		if ((width  + (xdiff)) - 1 > 0) {
			//real resize right
			this.setAreaSize(this.currentid, (width  + (xdiff)) - 1, null);
		}
		else {
			//jump to another state
			this.memory[this.currentid].width = 0;
			this.memory[this.currentid].left  = x;
			this.is_drawing = this.DM_RECTANGLE_RESIZE_LEFT;
		}
	}
	else if (this.is_drawing == this.DM_RECTANGLE_RESIZE_TOP) {
		this.fireEvent('onResizeArea', this.currentid);
		ydiff = y - top;
		if ((height + (-1 * ydiff)) > 0) {
			//real resize top
			this.areas[this.currentid].style.top   = y + 1 + 'px';
			this.setAreaSize(this.currentid, null, (height + (-1 * ydiff)));
		}
		else {
			//jump to another state
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_RECTANGLE_RESIZE_BOTTOM;
		}
	}
	else if (this.is_drawing == this.DM_RECTANGLE_RESIZE_BOTTOM) {
		this.fireEvent('onResizeArea', this.currentid);
		ydiff = y - top - height;
		if ((height + (ydiff)) - 1 > 0) {
			//real resize bottom
			this.setAreaSize(this.currentid, null, (height + (ydiff)) - 1);
		}
		else {
			//jump to another state
			this.memory[this.currentid].height = 0;
			this.memory[this.currentid].top    = y;
			this.is_drawing = this.DM_RECTANGLE_RESIZE_TOP;
		}
	}

	//repaint canvas elements
	if (this.is_drawing) {
		this._repaint(this.areas[this.currentid], this.config.CL_DRAW_SHAPE, x, y);
		this._updatecoords(this.currentid);
	}

};


/**
 *	EVENT HANDLER: Handles mouseup on the image.
 *	Handles dragging and resizing.
 *	@param	e	The event object.
 */
imgmap.prototype.img_mouseup = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	//console.log('img_mouseup');
	//if (!this.props[this.currentid]) return;
	var pos = this._getPos(this.pic);
	// 3.3.3 fix
	var x = (this.isMSIE) ? (window.event.x - this.pic.offsetLeft) : (e.clientX - pos.x);
	var y = (this.isMSIE) ? (window.event.y - this.pic.offsetTop)  : (e.clientY - pos.y);
	x = x + this.pic_container.scrollLeft;
	y = y + this.pic_container.scrollTop;
	//for everything that is move or resize
	if (this.is_drawing != this.DM_RECTANGLE_DRAW &&
		this.is_drawing != this.DM_SQUARE_DRAW &&
		this.is_drawing != this.DM_POLYGON_DRAW &&
		this.is_drawing != this.DM_POLYGON_LASTDRAW &&
		this.is_drawing != this.DM_BEZIER_DRAW &&
		this.is_drawing != this.DM_BEZIER_LASTDRAW) {
		//end dragging
		this.draggedId = null;
		//finish state
		this.is_drawing = 0;
		this.statusMessage(this.strings.READY);
		this.relaxArea(this.currentid);
		if (this.areas[this.currentid] == this._getLastArea()) {
			//if (this.config.mode != "editor2") this.addNewArea();
			return;
		}
		this.memory[this.currentid].downx  = x;
		this.memory[this.currentid].downy  = y;
	}
};


/**
 *	EVENT HANDLER: Handles mousedown on the image.
 *	Handles beggining or end of draw, or polygon/bezier point set.
 *	@param	e	The event object.
 */
imgmap.prototype.img_mousedown = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	if (!this.areas[this.currentid] && this.config.mode != "editor2") {return;}
	//console.log('img_mousedown');
	var pos = this._getPos(this.pic);
	// 3.3.3 fix
	var x = (this.isMSIE) ? (window.event.x - this.pic.offsetLeft) : (e.clientX - pos.x);
	var y = (this.isMSIE) ? (window.event.y - this.pic.offsetTop)  : (e.clientY - pos.y);
	x = x + this.pic_container.scrollLeft;
	y = y + this.pic_container.scrollTop;

	// Handle the Shift state
	if (!e) {
		e = window.event;
	}

	if (e.shiftKey)	{
		if (this.is_drawing == this.DM_POLYGON_DRAW) {
			this.is_drawing = this.DM_POLYGON_LASTDRAW;
		}
		else if (this.is_drawing == this.DM_BEZIER_DRAW) {
			this.is_drawing = this.DM_BEZIER_LASTDRAW;
		}
	}
	//console.log(this.is_drawing);
	//this.statusMessage(x + ' - ' + y + ': ' + this.props[this.currentid].getElementsByTagName('select')[0].value);
	if (this.is_drawing == this.DM_POLYGON_DRAW || this.is_drawing == this.DM_BEZIER_DRAW) {
		//its not finish state yet
		this.areas[this.currentid].xpoints[this.areas[this.currentid].xpoints.length] = x - 5;
		this.areas[this.currentid].ypoints[this.areas[this.currentid].ypoints.length] = y - 5;
		this.memory[this.currentid].downx  = x;
		this.memory[this.currentid].downy  = y;
		return;
	}
	else if (this.is_drawing && this.is_drawing != this.DM_POLYGON_DRAW && this.is_drawing != this.DM_BEZIER_DRAW) {
		//finish any other state
		if (this.is_drawing == this.DM_POLYGON_LASTDRAW || this.is_drawing == this.DM_BEZIER_LASTDRAW) {
			//add last controlpoint and update coords
			this.areas[this.currentid].xpoints[this.areas[this.currentid].xpoints.length] = x - 5;
			this.areas[this.currentid].ypoints[this.areas[this.currentid].ypoints.length] = y - 5;
			this._updatecoords(this.currentid);
			this.is_drawing = 0;
			this._polygonshrink(this.areas[this.currentid]);
		}
		this.is_drawing = 0;
		this.statusMessage(this.strings.READY);
		this.relaxArea(this.currentid);
		if (this.areas[this.currentid] == this._getLastArea()) {
			//editor mode adds next area automatically
			if (this.config.mode != "editor2") {this.addNewArea();}
			return;
		}
		return;
	}

	if (this.config.mode == "editor2") {
		if (!this.nextShape) {return;}
		this.addNewArea();
		//console.log("init: " + this.nextShape);
		this.initArea(this.currentid, this.nextShape);
	}
	else if (this.areas[this.currentid].shape == 'undefined' || this.areas[this.currentid].shape == 'poly') {
		//var shape = (this.props[this.currentid]) ? this.props[this.currentid].getElementsByTagName('select')[0].value : this.nextShape;
		var shape = this.nextShape;
		if (!shape) {shape = 'rect';}
		//console.log("init: " + shape);
		this.initArea(this.currentid, shape);
	}
	if (this.areas[this.currentid].shape == 'poly') {
		this.is_drawing = this.DM_POLYGON_DRAW;
		this.statusMessage(this.strings.POLYGON_DRAW);

		this.areas[this.currentid].style.left = x + 'px';
		this.areas[this.currentid].style.top  = y + 'px';
		this.areas[this.currentid].style.width  = 0;
		this.areas[this.currentid].style.height = 0;
		this.areas[this.currentid].xpoints = [];
		this.areas[this.currentid].ypoints = [];
		this.areas[this.currentid].xpoints[0] = x;
		this.areas[this.currentid].ypoints[0] = y;
	}
	else if (this.areas[this.currentid].shape == 'bezier1') {
		this.is_drawing = this.DM_BEZIER_DRAW;
		this.statusMessage(this.strings.BEZIER_DRAW);

		this.areas[this.currentid].style.left = x + 'px';
		this.areas[this.currentid].style.top  = y + 'px';
		this.areas[this.currentid].style.width  = 0;
		this.areas[this.currentid].style.height = 0;
		this.areas[this.currentid].xpoints = [];
		this.areas[this.currentid].ypoints = [];
		this.areas[this.currentid].xpoints[0] = x;
		this.areas[this.currentid].ypoints[0] = y;
	}
	else if (this.areas[this.currentid].shape == 'rect') {
		this.is_drawing = this.DM_RECTANGLE_DRAW;
		this.statusMessage(this.strings.RECTANGLE_DRAW);

		this.areas[this.currentid].style.left = x + 'px';
		this.areas[this.currentid].style.top  = y + 'px';
		this.areas[this.currentid].style.width  = 0;
		this.areas[this.currentid].style.height = 0;
	}
	else if (this.areas[this.currentid].shape == 'circle') {
		this.is_drawing = this.DM_SQUARE_DRAW;
		this.statusMessage(this.strings.SQUARE_DRAW);

		this.areas[this.currentid].style.left = x + 'px';
		this.areas[this.currentid].style.top  = y + 'px';
		this.areas[this.currentid].style.width  = 0;
		this.areas[this.currentid].style.height = 0;
	}

	this._setBorder(this.currentid, 'DRAW');
	this.memory[this.currentid].downx  = x;
	this.memory[this.currentid].downy  = y;
};


/**
 *	Highlights a given area.
 *	Sets opacity and repaints.
 *	@date	2007.12.28. 18:23:00
 *	@param	id	The id of the area to blur.
 *	@param	flag	Modifier, possible values: grad - for gradual fade in
 */
imgmap.prototype.highlightArea = function(id, flag) {
	if (this.is_drawing) {return;}//exit if in drawing state
	if (this.areas[id] && this.areas[id].shape != 'undefined') {
		//area exists - highlight it
		this.fireEvent('onFocusArea', this.areas[id]);
		this._setBorder(id, 'HIGHLIGHT');
		var opacity = this.config.highlight_opacity;
		if (flag == 'grad') {
			//apply gradient opacity
			opacity = '-' + opacity;
		}
		this._setopacity(this.areas[id], this.config.CL_HIGHLIGHT_BG, opacity);
		this._repaint(this.areas[id], this.config.CL_HIGHLIGHT_SHAPE);
	}
};


/**
 *	Blurs a given area.
 *	Sets opacity and repaints.
 *	@date	2007.12.28. 18:23:26
 *	@param	id	The id of the area to blur.
 *	@param	flag	Modifier, possible values: grad - for gradual fade out
 */
imgmap.prototype.blurArea = function(id, flag) {
	if (this.is_drawing) {return;}//exit if in drawing state
	if (this.areas[id] && this.areas[id].shape != 'undefined') {
		//area exists - fade it back
		this.fireEvent('onBlurArea', this.areas[id]);
		this._setBorder(id, 'NORM');
		var opacity = this.config.norm_opacity;
		if (flag == 'grad') {
			//apply gradient opacity
			opacity = '-' + opacity;
		}
		this._setopacity(this.areas[id], this.config.CL_NORM_BG, opacity);
		this._repaint(this.areas[id], this.config.CL_NORM_SHAPE);
	}
};


/**
 *	EVENT HANDLER: Handles event of mousemove on imgmap areas.
 *	- changes cursor depending where we are inside the area (buggy in opera)
 *	- handles area resize
 *	- handles area move
 *	@url	http://evolt.org/article/Mission_Impossible_mouse_position/17/23335/index.html
 *	@url	http://my.opera.com/community/forums/topic.dml?id=239498&t=1217158015&page=1
 *	@author	adam
 *	@param	e	The event object.
 */
imgmap.prototype.area_mousemove = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	if (!this.is_drawing) {
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		//opera fix - adam - 04-12-2007 23:14:05
		if (this.isOpera) {
			e.layerX = e.offsetX;
			e.layerY = e.offsetY;
		}
		var xdiff = (this.isMSIE) ? (window.event.offsetX) : (e.layerX);
		var ydiff = (this.isMSIE) ? (window.event.offsetY) : (e.layerY);
		//this.log(obj.aid + ' : ' + xdiff + ',' + ydiff);
		var resizable = (obj.shape == 'rect' || obj.shape == 'circle');
		if (resizable && xdiff < 6 && ydiff > 6) {
			//move left
			obj.style.cursor = 'w-resize';
		}
		else if (resizable && xdiff > parseInt(obj.style.width, 10) - 6  && ydiff > 6) {
			//move right
			obj.style.cursor = 'e-resize';
		}
		else if (resizable && xdiff > 6 && ydiff < 6) {
			//move top
			obj.style.cursor = 'n-resize';
		}
		else if (resizable && ydiff > parseInt(obj.style.height, 10) - 6  && xdiff > 6) {
			//move bottom
			obj.style.cursor = 's-resize';
		}
		else {
			//move all
			obj.style.cursor = 'move';
		}
		if (obj.aid != this.draggedId) {
			//not dragged or different
			if (obj.style.cursor == 'move') {obj.style.cursor = 'default';}
			return;
		}
		//moved here from mousedown
		if (xdiff < 6 && ydiff > 6) {
			//move left
			if (this.areas[this.currentid].shape == 'circle') {
				this.is_drawing = this.DM_SQUARE_RESIZE_LEFT;
				this.statusMessage(this.strings.SQUARE_RESIZE_LEFT);
			}
			else if (this.areas[this.currentid].shape == 'rect') {
				this.is_drawing = this.DM_RECTANGLE_RESIZE_LEFT;
				this.statusMessage(this.strings.RECTANGLE_RESIZE_LEFT);
			}
		}
		else if (xdiff > parseInt(this.areas[this.currentid].style.width, 10) - 6  && ydiff > 6) {
			//move right
			if (this.areas[this.currentid].shape == 'circle') {
				this.is_drawing = this.DM_SQUARE_RESIZE_RIGHT;
				this.statusMessage(this.strings.SQUARE_RESIZE_RIGHT);
			}
			else if (this.areas[this.currentid].shape == 'rect') {
				this.is_drawing = this.DM_RECTANGLE_RESIZE_RIGHT;
				this.statusMessage(this.strings.RECTANGLE_RESIZE_RIGHT);
			}
		}
		else if (xdiff > 6 && ydiff < 6) {
			//move top
			if (this.areas[this.currentid].shape == 'circle') {
				this.is_drawing = this.DM_SQUARE_RESIZE_TOP;
				this.statusMessage(this.strings.SQUARE_RESIZE_TOP);
			}
			else if (this.areas[this.currentid].shape == 'rect') {
				this.is_drawing = this.DM_RECTANGLE_RESIZE_TOP;
				this.statusMessage(this.strings.RECTANGLE_RESIZE_TOP);
			}
		}
		else if (ydiff > parseInt(this.areas[this.currentid].style.height, 10) - 6  && xdiff > 6) {
			//move bottom
			if (this.areas[this.currentid].shape == 'circle') {
				this.is_drawing = this.DM_SQUARE_RESIZE_BOTTOM;
				this.statusMessage(this.strings.SQUARE_RESIZE_BOTTOM);
			}
			else if (this.areas[this.currentid].shape == 'rect') {
				this.is_drawing = this.DM_RECTANGLE_RESIZE_BOTTOM;
				this.statusMessage(this.strings.RECTANGLE_RESIZE_BOTTOM);
			}
		}
		else/*if (xdiff < 10 && ydiff < 10 ) */{
			//move all
			if (this.areas[this.currentid].shape == 'circle') {
				this.is_drawing = this.DM_SQUARE_MOVE;
				this.statusMessage(this.strings.SQUARE_MOVE);
				this.memory[this.currentid].rdownx = xdiff;
				this.memory[this.currentid].rdowny = ydiff;
			}
			else if (this.areas[this.currentid].shape == 'rect') {
				this.is_drawing = this.DM_RECTANGLE_MOVE;
				this.statusMessage(this.strings.RECTANGLE_MOVE);
				this.memory[this.currentid].rdownx = xdiff;
				this.memory[this.currentid].rdowny = ydiff;
			}
			else if (this.areas[this.currentid].shape == 'poly' || this.areas[this.currentid].shape == 'bezier1') {
				if (this.areas[this.currentid].xpoints) {
					for (var i=0, le = this.areas[this.currentid].xpoints.length; i<le; i++) {
						this.memory[this.currentid].xpoints[i] = this.areas[this.currentid].xpoints[i];
						this.memory[this.currentid].ypoints[i] = this.areas[this.currentid].ypoints[i];
					}
				}

				if (this.areas[this.currentid].shape == 'poly') {
					this.is_drawing = this.DM_POLYGON_MOVE;
					this.statusMessage(this.strings.POLYGON_MOVE);
				}
				else if (this.areas[this.currentid].shape == 'bezier1') {
					this.is_drawing = this.DM_BEZIER_MOVE;
					this.statusMessage(this.strings.BEZIER_MOVE);
				}

				this.memory[this.currentid].rdownx = xdiff;
				this.memory[this.currentid].rdowny = ydiff;
			}
		}

		//common memory settings (preparing to move or resize)
		this.memory[this.currentid].width  = parseInt(this.areas[this.currentid].style.width, 10);
		this.memory[this.currentid].height = parseInt(this.areas[this.currentid].style.height, 10);
		this.memory[this.currentid].top    = parseInt(this.areas[this.currentid].style.top, 10);
		this.memory[this.currentid].left   = parseInt(this.areas[this.currentid].style.left, 10);
		this._setBorder(this.currentid, 'DRAW');
		this._setopacity(this.areas[this.currentid], this.config.CL_DRAW_BG, this.config.draw_opacity);
	}
	else {
		//if drawing and not ie, have to propagate to image event
		this.img_mousemove(e);
	}
};


/**
 *	EVENT HANDLER: Handles event of mouseup on imgmap areas.
 *	Basically clears draggedId.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.area_mouseup = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	//console.log('area_mouseup');
	if (!this.is_drawing) {
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		if (this.areas[this.currentid] != obj) {
			//trying to draw on a different canvas,switch to this one
			if (typeof obj.aid == 'undefined') {
				this.log('Cannot identify target area', 1);
				return;
			}
			//this.form_selectRow(obj.aid, true);
			//this.currentid = obj.aid;
		}
		this.draggedId = null;
	}
	else {
		//if drawing and not ie, have to propagate to image event
		//console.log('propup');
		this.img_mouseup(e);
	}
};


/**
 *	EVENT HANDLER: Handles event of mouseover on imgmap areas.
 *	Calls gradual highlight on the given area.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.area_mouseover = function(e) {
	if (this.viewmode === 1 && this.config.mode !== 'highlighter_spawn') {return;}//exit if preview mode
	if (!this.is_drawing) {
		//this.log('area_mouseover');
		//identify source object
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		/*
		//switch to hovered area
		if (this.areas[this.currentid] != obj) {
			//trying to draw on a different canvas, switch to this one
			if (typeof obj.aid == 'undefined') {
				this.log('Cannot identify target area', 1);
				return;
			}
			this.form_selectRow(obj.aid, true);
			this.currentid = obj.aid;
		}
		*/
		this.highlightArea(obj.aid, 'grad');
	}
};


/**
 *	EVENT HANDLER: Handles event of mouseout on imgmap areas.
 *	Calls gradient blur on the given area.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.area_mouseout = function(e) {
	if (this.viewmode === 1 && this.config.mode !== 'highlighter_spawn') {return;}//exit if preview mode
	if (!this.is_drawing) {
		//this.log('area_mouseout');
		//identify source object
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		this.blurArea(obj.aid, 'grad');
	}
};


/**
 *	EVENT HANDLER: Handles event of double click on imgmap areas.
 *	Basically only fires the custom callback.
 *	@author	Colin Bell
 *	@param	e	The event object
 */
imgmap.prototype.area_dblclick = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	//console.log('area_dblclick');
	if (!this.is_drawing) {
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		if (this.areas[this.currentid] != obj) {
			//trying to draw on a different canvas, switch to this one
			if (typeof obj.aid == 'undefined') {
				this.log('Cannot identify target area', 1);
				return;
			}
			this.currentid = obj.aid;
		}
		this.fireEvent('onDblClickArea', this.areas[this.currentid]);
		//stop event propagation to document level
		if (this.isMSIE) {
			window.event.cancelBubble = true;
		}
		else {
			e.stopPropagation();
		}
	}
};


/**
 *	EVENT HANDLER: Handles event of mousedown on imgmap areas.
 *	Sets the variables draggedid, selectedid and currentid to the given area.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.area_mousedown = function(e) {
	if (this.viewmode === 1 && this.config.mode !== 'highlighter_spawn') {return;}//exit if preview mode
	//console.log('area_mousedown');
	if (!this.is_drawing) {
		var obj = (this.isMSIE) ? window.event.srcElement : e.currentTarget;
		if (obj.tagName == 'DIV') {
			//do this because of label
			obj = obj.parentNode;
		}
		if (obj.tagName == 'image' || obj.tagName == 'group' ||
			obj.tagName == 'shape' || obj.tagName == 'stroke') {
			//do this because of excanvas
			obj = obj.parentNode.parentNode;
		}
		if (this.areas[this.currentid] != obj) {
			//trying to draw on a different canvas, switch to this one
			if (typeof obj.aid == 'undefined') {
				this.log('Cannot identify target area', 1);
				return;
			}
			this.currentid = obj.aid;
		}
		//this.log('selected = '+this.currentid);
		this.draggedId  = this.currentid;
		this.selectedId = this.currentid;
		this.fireEvent('onSelectArea', this.areas[this.currentid]);
		//stop event propagation to document level
		if (this.isMSIE) {
			window.event.cancelBubble = true;
		}
		else {
			e.stopPropagation();
		}
	}
	else {
		//if drawing and not ie, have to propagate to image event
		//console.log('propdown');
		this.img_mousedown(e);
	}
};


/**
 *	EVENT HANDLER: Handles event 'keydown' on document.
 *	Handles SHIFT hold while drawing.
 *	Note: Safari doesn't generate keyboard events for modifiers:
 *	@url	http://bugs.webkit.org/show_bug.cgi?id=11696
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.doc_keydown = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	var key = (this.isMSIE) ? event.keyCode : e.keyCode;
	//console.log(key);
	if (key == 46) {
		//delete key pressed
		if (this.selectedId !== null && !this.is_drawing) {this.removeArea(this.selectedId);}
	}
	else if (key == 16) {
		//shift key pressed
		if (this.is_drawing == this.DM_RECTANGLE_DRAW) {
			this.is_drawing = this.DM_SQUARE_DRAW;
			this.statusMessage(this.strings.SQUARE2_DRAW);
		}
	}
};


/**
 *	EVENT HANDLER: Handles event 'keyup' on document.
 *	Handles SHIFT release while drawing.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.doc_keyup = function(e) {
	var key = (this.isMSIE) ? event.keyCode : e.keyCode;
	//alert(key);
	if (key == 16) {
		//shift key released
		if (this.is_drawing == this.DM_SQUARE_DRAW && this.areas[this.currentid].shape == 'rect') {
			//not for circle!
			this.is_drawing = this.DM_RECTANGLE_DRAW;
			this.statusMessage(this.strings.RECTANGLE_DRAW);
		}
	}
};


/**
 *	EVENT HANDLER: Handles event 'mousedown' on document.
 *	@author	adam
 *	@param	e	The event object
 */
imgmap.prototype.doc_mousedown = function(e) {
	if (this.viewmode === 1) {return;}//exit if preview mode
	if (!this.is_drawing) {
		this.selectedId = null;
	}
};


/**
 *	Get the real position of the element.
 *	Deal with browser differences when trying to get the position of an area.
 *	@param	element	The element you want the position of.
 *	@return	An object with x and y members.
 */
imgmap.prototype._getPos = function(element) {
	var xpos = 0;
	var ypos = 0;
	if (element) {
		var elementOffsetParent = element.offsetParent;
		// If the element has an offset parent
		if (elementOffsetParent) {
			// While there is an offset parent
			while ((elementOffsetParent = element.offsetParent)) {
				//offset might give negative in opera when the image is scrolled
				if (element.offsetLeft > 0) {xpos += element.offsetLeft;}
				if (element.offsetTop > 0) {ypos += element.offsetTop;}
				element = elementOffsetParent;
			}
			// handle positioned element in Chrome
			if (element.offsetLeft > 0) {xpos += element.offsetLeft;}
			if (element.offsetTop > 0) {ypos += element.offsetTop;}
		}
		else {
			xpos = element.offsetLeft;
			ypos = element.offsetTop;
		}
	}
	return {x: xpos, y: ypos};
};


/**
 *	Gets the last (visible and editable) area.
 *	@author	Adam Maschek (adam.maschek(at)gmail.com)
 *	@date	2006-06-15 16:34:51
 *	@returns	The last area object or null.
 */
imgmap.prototype._getLastArea = function() {
	for (var i = this.areas.length-1; i>=0; i--) {
		if (this.areas[i]) {
			return this.areas[i];
		}
	}
	return null;
};


/**
 *	Parses cssText to single style declarations.
 *	@author	adam
 *	@date	25-09-2007 18:19:51
 *	@param obj	The DOM object to apply styles on.
 *	@param cssText	The css declarations to apply.
 */
imgmap.prototype.assignCSS = function(obj, cssText) {
	var parts = cssText.split(';');
	for (var i = 0; i < parts.length; i++) {
		var p = parts[i].split(':');
		//we need to camelcase by - signs
		var pp = this.trim(p[0]).split('-');
		var prop = pp[0];
		for (var j = 1; j < pp.length; j++) {
			//replace first letters to uppercase
			prop+= pp[j].replace(/^\w/, pp[j].substring(0,1).toUpperCase());
		}
		obj.style[this.trim(prop)] = this.trim(p[1]);
	}
};


/**
 *	To fire callback hooks on custom events, passing them the object of the event.
 *	@author	adam
 *	@date	13-10-2007 15:24:49
 *	@param evt	The type of event
 *	@param obj	The object of the event. (can be an id, a string, an object, whatever is most relevant)
 */
imgmap.prototype.fireEvent = function(evt, obj) {
	//this.log("Firing event: " + evt);
	if (typeof this.config.custom_callbacks[evt] == 'function') {
		return this.config.custom_callbacks[evt](obj);
	}
};


/**
 *	To set area dimensions.
 *	This is needed to achieve the same result in all browsers.
 *	@author	adam
 *	@date	10-12-2007 22:29:41
 *	@param	id	The id of the area (canvas) to resize.
 *	@param	w	The desired width in pixels.
 *	@param	h	The desired height in pixels.
 */
imgmap.prototype.setAreaSize = function(id, w, h) {
	if (id === null) {id = this.currentid;}
	if (w !== null) {
		this.areas[id].width  = w;
		this.areas[id].style.width  = (w) + 'px';
		this.areas[id].setAttribute('width',  w);
	}
	if (h !== null) {
		this.areas[id].height = h;
		this.areas[id].style.height = (h) + 'px';
		this.areas[id].setAttribute('height', h);
	}
};


/**
 *	Tries to detect preferred language of user.
 *	@date	2007.12.28. 15:43:46
 *	@return The two byte language code. (We dont care now for pt-br, etc.)
 */
imgmap.prototype.detectLanguage = function() {
	var lang;
	if (navigator.userLanguage) {
		lang = navigator.userLanguage.toLowerCase();
	}
	else if (navigator.language) {
		lang = navigator.language.toLowerCase();
	}
	else {
		return this.config.defaultLang;
	}
	//this.log(lang, 2);
	if (lang.length >= 2) {
		lang = lang.substring(0,2);
		return lang;
	}
	return this.config.defaultLang;
};


/**
 *	Disable selection on a given object.
 *	This is especially useful in Safari, where dragging around areas
 *	keeps selecting all sorts of things.
 *	@author	Bret Taylor
 *	@url	http://ajaxcookbook.org/disable-text-selection/
 *	@date	27-07-2008 1:57:45
 *	@param	element	The DOM element on which you want to disable selection.
 */
imgmap.prototype.disableSelection = function(element) {
	if (typeof element == 'undefined' || !element) {return false;}
	if (typeof element.onselectstart != "undefined") {
	    element.onselectstart = function() {
	        return false;
	    };
    }
    if (typeof element.unselectable != "undefined") {
    	element.unselectable = "on";
    }
    if (typeof element.style.MozUserSelect != "undefined") {
    	element.style.MozUserSelect = "none";
    }
};


/**
 *	@date	11-02-2007 19:57:05
 *	@url	http://www.deepwood.net/writing/method-references.html.utf8
 *	@author	Daniel Brockman
 *	@addon
 */
Function.prototype.bind = function(object) {
	var method = this;
	return function () {
		return method.apply(object, arguments);
	};
};


/**
 *	Trims a string.
 *	Changed not to extend String but use own function for better compatibility.
 *	@param str The string to trim.
 */
imgmap.prototype.trim = function(str) {
	return str.replace(/^\s+|\s+$/g, '');
};


/**
 *	Spawn an imgmap object for each imagemap found in the document.
 *	This is used for highlighter mode only.
 *	@param config	An imgmap config object
 */
function imgmap_spawnObjects(config) {
	//console.log('spawnobjects');
	var maps = document.getElementsByTagName('map');
	var imgs = document.getElementsByTagName('img');
	var imaps = [];
	var imapn;
	//console.log(maps.length);
	for (var i=0, le=maps.length; i<le; i++) {
		for (var j=0, le2=imgs.length; j<le2; j++) {
		//console.log(i);
		//	console.log(maps[i].name);
		//	console.log(imgs[j].getAttribute('usemap'));
			if ('#' + maps[i].name == imgs[j].getAttribute('usemap')) {
				//we found one matching pair
			//	console.log(maps[i]);
				config.mode = 'highlighter_spawn';
				imapn = new imgmap(config);
				//imapn.setup(config);
				imapn.useImage(imgs[j]);
				imapn.setMapHTML(maps[i]);
				imapn.viewmode = 1;

				imaps.push(imapn);

			}
		}
	}
}

//global instance?
//imgmap_spawnObjects();?