Source: render/kekule.render.utils.js

/**
 * @fileoverview
 * Utility classes and methods for rendering 2D or 3D molecule structures.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /utils/kekule.utils.js
 * requires /core/kekule.structures.js
 * requires /render/kekule.render.base.js
 * requires /render/kekule.render.extensions.js
 * requires /render/kekule.render.renderColorData.js
 */

/**
 * Contains constants for rich text manipulation.
 * @class
 */
Kekule.Render.RichText = {
	/** Indicate an rich text item is text section. */
	SECTION: 'section',
	/** Indicate an rich text item is group. */
	GROUP: 'group',
	/** Indicate an rich text group contains multiline. */
	LINES: 'lines',
	/** Superscript. */
	SUP: 'superscript',
	/** Subscript */
	SUB: 'subscript',
	/** Normal text */
	NORMAL: 'normal'
};

/**
 *  Methods to manipulate rich format text.
 *  A rich format text is consists of a array of objects. For example:
 *    {
 *    	role: 'seq',
 *    	anchorItem: itemRef,  // or default first item, must be the direct child of group or seq
 *    	items: [
 *        {
 *          role: 'lines',  // this special role is used in multiline text
 *          items: [
 *          {
 *            role: 'section',
 *            text: 'Text1',
 *            //font: 'arial bold italic 10px',
 *            textType: 'subscript',
 *            refItem: anRichTextItem,   // the subscript or superscript is attached to which one? If not set, regard prev one as refItem
 *            horizontalAlign: 1,   // value from Kekule.Render.TextAlign
 *            verticalAlign: 1,
 *            charDirection: 1,
 *            overhang: 0.1,
 *            oversink: 0.1,
 * 					  _noAlignRect: true,  // a special property to tell the drawer that this item should not be considered into align box.
 * 				  									// super/subscript defaultly has noAlign = true
 *          },
 *          {
 *            role: 'section'
 *            text: 'Text2',
 *            fontSize: '20px',
 *            textType: 'normal'
 *          },
 *          {
 *            role: 'group',
 *            anchorItem: itemRef,
 *            items: [...]
 *          }
 *          ]
 *       ]
 *    }
 *  @class
 */
Kekule.Render.RichTextUtils = {
	/** @private */
	STYLE_PROPS: ['fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'color', 'opacity'],
	/**
	 * Create a new and empty rich text object.
	 */
	create: function()
	{
		//return {'items': []};
		return Kekule.Render.RichTextUtils.createGroup(/*'seq'*/);
	},
	/**
	 * Create a new group.
	 * @param {String} role Role of group, if not set, a normal role of 'group' will be created.
	 */
	createGroup: function(role, style)
	{
		var result = {'role': role || 'group', 'items': []};
		if (style)
		{
			for (var p in style)
			{
				if (style.hasOwnProperty(p))
				{
					result[p] = style[p];
				}
			}
		}
		return result;
	},
	/**
	 * Create a section object of richText (an item in richtext array).
	 * @param {String} text
	 * @param {Hash} style
	 */
	createSection: function(text, style)
	{
		var section = {'text': text};
		if (style)
		{
			for (var p in style)
			{
				if (style.hasOwnProperty(p))
				{
					section[p] = style[p];
				}
			}
		}
		return section;
	},
	/**
	 * Convert a plain string to rich format text.
	 * Multiline is supported.
	 * @param {String} str
	 * @param {Hash} style Can be null
	 * @returns {Object}
	 */
	strToRichText: function(str, style)
	{
		if (!str)
			str = '';
		var RTU = Kekule.Render.RichTextUtils;
		var lines = str.split('\n');
		//console.log('split to', lines);
		if (lines.length <= 1)
			return Kekule.Render.RichTextUtils.appendText(RTU.create(), str, style);
		else  // multiline
		{
			var result = RTU.createGroup(Kekule.Render.RichText.LINES);
			for (var i = 0, l = lines.length; i < l; ++i)
			{
				var line = lines[i] || '\u00a0';  // unicode non-break blank, to insert a blank line  // TODO: may need a better solution
				RTU.appendText(result, line, style);
			}
			//console.log('rich text', result);
			return result;
		}
	},
	/**
	 * Insert a styled text to a special position of richText group.
	 * @param {Object} richTextGroup
	 * @param {Int} index
	 * @param {String} text
	 * @param {Hash} style Can be null.
	 * @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
	 * @returns {Object}
	 */
	insertText: function(richTextGroup, index, text, style, isAnchor)
	{
		var section = Kekule.Render.RichTextUtils.createSection(text, style);
		//richText.splice(index, 0, section);
		richTextGroup.items.splice(index, 0, section);
		if (isAnchor)
			richTextGroup.anchorItem = section;
		return richTextGroup;
	},
	/**
	 * Append a styled text to richText group and returns the whole group.
	 * @param {Object} richTextGroup
	 * @param {String} text
	 * @param {Hash} style Can be null.
	 * @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
	 * @returns {Object} richTextGroup
	 */
	appendText: function(richTextGroup, text, style, isAnchor)
	{
		var section = Kekule.Render.RichTextUtils.createSection(text, style);
		richTextGroup.items.push(section);
		if (isAnchor)
			richTextGroup.anchorItem = section;
		return richTextGroup;
	},
	/**
	 * Append a styled text to richText group and returns the new section created.
	 * @param {Object} richTextGroup
	 * @param {String} text
	 * @param {Hash} style Can be null.
	 * @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
	 * @returns {Object} New section appended to richTextGroup.
	 */
	appendText2: function(richTextGroup, text, style, isAnchor)
	{
		var section = Kekule.Render.RichTextUtils.createSection(text, style);
		richTextGroup.items.push(section);
		if (isAnchor)
			richTextGroup.anchorItem = section;
		return section;
	},
	/**
	 * Insert a group or section to a special position in destGroup.
	 * @param {Object} destGroup
	 * @param {Int} index
	 * @param {Object} groupOrSection
	 * @returns {Object}
	 */
	insert: function(destGroup, index, groupOrSection)
	{
		destGroup.items.splice(index, 0, groupOrSection);
		return destGroup;
	},
	/**
	 * Append a group or section to tail in destGroup.
	 * @param {Object} destGroup
	 * @param {Object} groupOrSection
	 * @returns {Object}
	 */
	append: function(destGroup, groupOrSection)
	{
		destGroup.items.push(groupOrSection);
		return destGroup;
	},
	/**
	 * Append a set of groups or sections to tail in destGroup.
	 * @param {Object} destGroup
	 * @param {Array} items
	 * @returns {Object}
	 */
	appendItems: function(destGroup, items)
	{
		destGroup.items = destGroup.items.concat(Kekule.ArrayUtils.toArray(items));
		return destGroup;
	},

	/**
	 * Returns type (section or group) of item.
	 * @param {Object} item
	 * @returns {String} Constant value from {@link Kekule.Render.RichText}.
	 */
	getItemType: function(item)
	{
		if (item.items)
		{
			//return Kekule.Render.RichText.GROUP;
			return item.role;  // GROUP or LINE
		}
		else
		{
			return Kekule.Render.RichText.SECTION;
		}
	},
	/**
	 * Returns role (normal, sup or sub) of item.
	 * @param {Object} item
	 * @returns {String} Constant value from {@link Kekule.Render.RichText}
	 */
	getItemRole: function(item)
	{
		//return /*item.role || Kekule.Render.RichText.NORMAL;*/
		var R = Kekule.Render.RichText;
		return (Kekule.Render.RichTextUtils.getItemType(item) === R.GROUP)? R.GROUP: R.SECTION;
	},
	/**
	 * Return text type (normal, superscript or subscript) of item.
	 * @param {Object} item
	 * @returns {String} Constant value from {@link Kekule.Render.RichText}
	 */
	getItemTextType: function(item)
	{
		return item.textType || Kekule.Render.RichText.NORMAL;
	},
	/**
	 * Check if item is a superscript.
	 * @param {Object} item
	 * @returns {Bool}
	 */
	isSuperscript: function(item)
	{
		return Kekule.Render.RichTextUtils.getItemTextType(item) == Kekule.Render.RichText.SUP;
	},
	/**
	 * Check if item is a superscript.
	 * @param {Object} item
	 * @returns {Bool}
	 */
	isSubscript: function(item)
	{
		return Kekule.Render.RichTextUtils.getItemTextType(item) == Kekule.Render.RichText.SUB;
	},
	/**
	 * Check if an item is a rich text group.
	 * @param {Object} item
	 * @returns {Bool}
	 */
	isGroup: function(item)
	{
		return Kekule.Render.RichTextUtils.getItemType(item) === Kekule.Render.RichText.GROUP;
	},
	/**
	 * Check if an item is a rich text section.
	 * @param {Object} item
	 * @returns {Bool}
	 */
	isSection: function(item)
	{
		return Kekule.Render.RichTextUtils.getItemType(item) === Kekule.Render.RichText.SECTION;
	},

	/**
	 * Returns the first normal text (nor sub/superscript) in rich text.
	 * @param {Object} richTextGroup
	 * @returns {Object}
	 */
	getFirstNormalTextSection: function(richTextGroup)
	{
		for (var i = 0, l = richTextGroup.items.length; i < l; ++i)
		{
			var item = richTextGroup.items[i];
			var textType = Kekule.Render.RichTextUtils.getItemTextType(item);
			if (textType === Kekule.Render.RichText.NORMAL)
				return item;
		}
		return null;
	},

	/**
	 * Returns the actual refItem of item. Generally this function returns item.refItem,
	 * however, if that value is not set, function will return item's nearest sibling.
	 * @param {Object} item
	 * @param {Object} parent item's parent group.
	 * @returns {Object}
	 */
	getActualRefItem: function(item, parent)
	{
		var result = item.refItem;
		if ((!result) && (parent.items.length > 1))  // check sibling
		{
			var index = parent.items.indexOf(item);
			if (index > 0)
				result = parent.items[index - 1];
			else if (index == 0)
				result = parent.items[1];
			else
				result = null;
		}
		return result;
	},

	/**
	 * Find the real anchor item in richText cascadely.
	 * @param {Object} richText
	 * @returns {Object}
	 */
	getFinalAnchorItem: function(richText)
	{
		if (richText.anchorItem)
			return Kekule.Render.RichTextUtils.getFinalAnchorItem(richText.anchorItem);
		else
			return richText;
	},

	/**
	 * Tidy the rich text and merge groups with same style.
	 * @param {Object} richText
	 * @returns {Object}
	 */
	tidy: function(richText)
	{
		var result = Kekule.Render.RichTextUtils.createGroup(richText.role);
		var currIndex = -1;
		for (var i = 0, l = richText.items.length; i < l; ++i)
		{
			var item = richText.items[i];
			if (item.items) // is group
			{
				var newGroup = Kekule.Render.RichTextUtils.tidy(item);
				// check if newGroup has only one section, if true, convert it into a section
				if (newGroup.items.length == 1)
				{
					var newItem = Object.extend({}, newGroup);
					delete newItem.items;
					delete newItem.role;
					newItem = Object.extend(newItem, newGroup.items[0]);
					item = newItem;
					// then try merge items
				}
				else if (newGroup.items.length > 0)
				{
					++currIndex;
					result.items.push(newGroup);
					continue;
				}
			}
			//else // text item, try merge
 			{
				var merged = false;
				if (currIndex >= 0)
				{
					var prevItem = result.items[currIndex];
					if (!prevItem.items)  // not group, just text
					{
						// check if item and prevItem has the same style
						if (Kekule.ObjUtils.equal(item, prevItem, ['text']))
						{
							prevItem.text += item.text;
							merged = true;
						}
					}
				}
				if (!merged)
				{
					++currIndex;
					result.items.push(Object.extend({}, item));
				}
			}
		}
		return result;
	},

	/**
	 * Clone source rich text.
	 * @param {Object} richText
	 * @returns {Object}
	 */
	clone: function(richText)
	{
		var result = {};
		Object.extend(result, richText);
		if (richText.items)
		{
			result.items = [];
			for (var i = 0, l = richText.items.length; i < l; ++i)
			{
				var item = {};
				item = Kekule.Render.RichTextUtils.clone(richText.items[i]);
				result.items.push(item);
				/*
				if (richText.anchorItem && (richText.anchorItem == richText.items[i]))
					result.anchorItem = item;
				*/
			}
		}
		// RefItem and AnchorItem pointer should be handled carefully

		if (result.items)
		{
			if (richText.anchorItem)
			{
				var index = richText.items.indexOf(richText.anchorItem);
				if (index >= 0)
					result.anchorItem = result.items[index]
				else
					result.anchorItem = null;
			}

			for (var i = 0, l = result.items.length; i < l; ++i)
			{
				var originItem = richText.items[i];
				var item = result.items[i];
				if (originItem.refItem)
				{
					var index = richText.items.indexOf(originItem.refItem);
					if (index >= 0)
						item.refItem = result.items[index]
					else
						item.refItem = null;
				}
			}
		}
		return result;
	},

	/**
	 * Extract text in rich text and returns a pure string.
	 * @param {Object} richText
	 * @returns {String}
	 */
	toText: function(richText)
	{
		var result = '';
		if (richText.items)
		{
			for (var i = 0, l = richText.items.length; i < l; ++i)
			{
				var item = richText.items[i];
				if (item.items)  // group
					result += Kekule.Render.RichTextUtils.toText(item);
				else if (item.text)  // plain text
				{
					result += item.text;
				}
			}
		}
		else
			return richText.text || '';
		return result;
	},

	/** @private */
	_toDebugHtml: function(richText)
	{
		if (!richText.items)  // only one text part
			return richText.text;
		var result = '';
		for (var i = 0, l = richText.items.length; i < l; ++i)
		{
			var item = richText.items[i];
			if (item.items)  // group
				result += Kekule.Render.RichTextUtils._toDebugHtml(item);
			else if (item.text)  // plain text
			{
				switch (item.textType)
				{
					case Kekule.Render.RichText.SUB:
						result += '<sub>' + item.text + '</sub>';
						break;
					case Kekule.Render.RichText.SUP:
						result += '<sup>' + item.text + '</sup>';
						break;
					default:
						result += item.text;
				}
			}
		}
		return result;
	},

	/**
	 * Convert rich text to HTML code in a simple way.
	 * @param {Object} richText
	 * @returns {HTMLElement}
	 */
	toSimpleHtmlCode: function(richText)
	{
		return Kekule.Render.RichTextUtils._toDebugHtml(richText);
	},

	/**
	 * Convert rich text to HTML element.
	 * Note: in the conversion, vertical lines are all turned into horizontal lines
	 * @param {Document} doc Owner document of result element.
	 * @param {Object} richText
	 * @param {Bool} reversedDirection
	 * @returns {HTMLElement}
	 */
	toHtml: function(doc, richText, reversedDirection)
	{
		var RT = Kekule.Render.RichText;
		var result;

		var reversed;
		if (richText.charDirection)
			reversed = richText.charDirection === Kekule.Render.TextDirection.RTL
				|| richText.charDirection === Kekule.Render.TextDirection.BTT;
		else
			reversed = reversedDirection;


		var role = richText.role;
		if (role === RT.SECTION || richText.text)  // text section
		{
			var textType = richText.textType;
			var tagName = (textType === RT.SUB)? 'sub':
					(textType === RT.SUP)? 'sup':
					'span';
			result = doc.createElement(tagName);
			var text = richText.text;
			if (reversed)
				text = text.reverse();
			Kekule.DomUtils.setElementText(result, richText.text || '');
		}
		else // line or group
		{
			var childEmbedTagName = null;
			var childEmbedStyleText;
			if (role === RT.LINES)
			{
				result = doc.createElement('div');
				childEmbedTagName = 'p';
				childEmbedStyleText = 'margin:0.2em 0;padding:0';
			}
			else // group
			{
				result = doc.createElement('span');
			}
			// convert children
			var lastChildElem;
			for (var i = 0, l = richText.items.length; i < l; ++i)
			{
				var item = richText.items[i];
				var childElem = Kekule.Render.RichTextUtils.toHtml(doc, item, reversed);
				if (childEmbedTagName)
				{
					var embedElem = doc.createElement(childEmbedTagName);
					if (childEmbedStyleText)
						embedElem.style.cssText = childEmbedStyleText;
					embedElem.appendChild(childElem);
					childElem = embedElem;
				}
				if (!reversed || !lastChildElem)
					result.appendChild(childElem);
				else
					result.insertBefore(childElem, lastChildElem);
				lastChildElem = childElem;
			}
		}

		// styles
		var elemStyle = result.style;
		var styleProps = Kekule.Render.RichTextUtils.STYLE_PROPS;
		for (var i = 0, l = styleProps.length; i < l; ++i)
		{
			var prop = styleProps[i];
			if (richText[prop])
			{
				elemStyle[prop] = richText[prop];
			}
		}

		return result;
	},

	/**
	 * Create new rich text from HTML element.
	 * @param {Element} htmlElement
	 * @returns {Object} Created richtext object.
	 */
	fromHtml: function(htmlElement)
	{
		var RT = Kekule.Render.RichText;
		var RTU = Kekule.Render.RichTextUtils;
		var DU = Kekule.DomUtils;
		var SU = Kekule.StyleUtils;

		function _createRichTextFromHtmlNode(node, isRoot)
		{
			if (node.nodeType === Node.TEXT_NODE)  // pure text node, create a section
			{
				var text = node.nodeValue;
				// erase line breaks
				text = text.replace(/[\r\n]/g, '');
				return text.trim()? RTU.createSection(text): null; // ignore all space text
			}
			else if (node.nodeType === Node.ELEMENT_NODE)
			{
				var stylePropNames = RTU.STYLE_PROPS;
				stylePropNames.push('direction');
				var styles = {};
				// extract styles from HTML
				for (var i = 0, l = stylePropNames.length; i < l; ++i)
				{
					var stylePropName = stylePropNames[i];
					var value;
					if (isRoot)  // is root element, consider computed styles
					{
						value = SU.getComputedStyle(node, stylePropName);
					}
					else  // child elements, only consider inline styles
					{
						value = node.style[stylePropName];
					}
					if (value)
						styles[stylePropName] = value;
				}
				// char direction
				if (!styles.direction)
					styles.charDirection = Kekule.Render.TextDirection.DEFAULT;
				else
				{
					styles.charDirection = (styles.direction === 'ltr')? Kekule.Render.TextDirection.LTR:
							(styles.direction === 'rtl')? Kekule.Render.TextDirection.RTL:
							(styles.direction === 'inherit')? Kekule.Render.TextDirection.INHERIT:
							null;
					delete styles.direction;
				}

				// Check if element has children elements
				if (!DU.getFirstChildElem(node))  // no child element, may elem only contains text
				{
					var text = DU.getElementText(node);
					var tagName = node.tagName.toLowerCase();
					if (tagName === 'sub')
						styles.textType = RT.SUB;
					else if (tagName === 'sup')
						styles.textType = RT.SUP;
					else
						styles.textType = RT.NORMAL;
					return text? RTU.createSection(text, styles): null;
				}
				else  // iterate through children
				{
					var resultRole = RT.GROUP;
					var childNodes = DU.getChildNodesOfTypes(node, [Node.ELEMENT_NODE, Node.TEXT_NODE]);
					var childRTs = [];
					var hasLines = false;
					for (var i = 0, l = childNodes.length; i < l; ++i)
					{
						var child = childNodes[i];
						var childRT = _createRichTextFromHtmlNode(child, false);
						if (child.nodeType === Node.ELEMENT_NODE)  //
						{
							if (child.tagName.toLowerCase() === 'br')  // explicit line break
							{
								// push special flags
								childRTs.push('br');
								hasLines = true;
							}
							var isBlockElem = SU.isBlockElem(child);
							if (isBlockElem)
							{
								hasLines = true;
								childRT._isLine = true; // markup it is a text line
							}
						}
						if (childRT)
						{
							childRTs.push(childRT);
							//RTU.append(result, childRT);
						}
					}
					if (hasLines)  // need to group childRTs into lines
					{
						var linedChildRTs = [];
						var unpushedChildren = [];
						for (var i = 0, l = childRTs.length; i < l; ++i)
						{
							var childRT = childRTs[i];
							if (childRT._isLine || childRT == 'br')  // special break flag
							{
								if (unpushedChildren.length)  // create a new group to include all unhandled inline sections
								{
									if (unpushedChildren.length > 1)
									{
										var prevGroup = RTU.createGroup();
										RTU.appendItems(prevGroup, unpushedChildren);
										linedChildRTs.push(prevGroup);
									}
									else
										linedChildRTs.push(unpushedChildren[0]);
									unpushedChildren = [];
								}
								if (childRT._isLine)
								{
									delete childRT._isLine;
									linedChildRTs.push(childRT);
								}
							}
							else
								unpushedChildren.push(childRT);
						}
						if (unpushedChildren.length)
						{
							if (unpushedChildren.length > 1)
							{
								var prevGroup = RTU.createGroup();
								RTU.appendItems(prevGroup, unpushedChildren);
								linedChildRTs.push(prevGroup);
							}
							else
								linedChildRTs.push(unpushedChildren[0]);
						}
						childRTs = linedChildRTs;
					}
					// at last push childRTs to result
					if (hasLines)
						resultRole = RT.LINES;

					var result = RTU.createGroup(resultRole, styles);
					for (var i = 0, l = childRTs.length; i < l; ++i)
					{
						RTU.append(result, childRTs[i]);
					}
					return result;
				}
			}
		}

		return _createRichTextFromHtmlNode(htmlElement, true);
	}
};

/**
 * Methods about chem information displaying and rich text.
 * @class
 */
Kekule.Render.ChemDisplayTextUtils = {
	/** @private */
	//RADICAL_LABELS: ['', '••', '•', '••'],
	RADICAL_LABELS: ['', '\u2022\u2022', '\u2022', '\u2022\u2022'],
	RADICAL_TRIPLET_ALTER_LABEL: '^^',
	/**
	 * Returns suitable text to indicate the radical.
	 * @param {Int} radical
	 * @returns {String}
	 */
	getRadicalDisplayText: function(radical, useAlterTripletRadicalMark)
	{
		if (useAlterTripletRadicalMark && radical === Kekule.RadicalOrder.TRIPLET)
			return Kekule.Render.ChemDisplayTextUtils.RADICAL_TRIPLET_ALTER_LABEL;
		else
			return Kekule.Render.ChemDisplayTextUtils.RADICAL_LABELS[radical];
	},
	/**
	 * Returns text to represent atom charge and radical (e.g., 2+).
	 * @param {Number} charge
	 * @param {Int} radical
	 * @param {Int} partialChargeDecimalsLength
	 * @param {Int} chargeMarkType
	 * @returns {String}
	 */
	getChargeDisplayText: function(charge, partialChargeDecimalsLength, chargeMarkType)
	{
		var slabel = '';
		var showCharge = (!!charge) && (!partialChargeDecimalsLength || (Math.abs(charge) > Math.pow(10, -partialChargeDecimalsLength)/2));
		if (showCharge)
		{
			var chargeSign = (charge > 0)? '+': '-';
			var chargeAmount = Math.abs(charge);
			if (chargeAmount != 1)
			{
				slabel += partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString();
			}
			else  // +1 or -1, may use different charge sign char
			{
				if (chargeMarkType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND)
					chargeSign = (charge > 0)? '\u2295': '\u2296';
			}
			slabel += chargeSign;
		}
		return slabel;
	},
	/**
	 * Create a rich text section (usually superscript) to display atom charge and radical.
	 * @param {Number} charge
	 * @param {Int} radical
	 * @param {Int} partialChargeDecimalsLength
	 * @param {Int} chargeMarkType
	 * @returns {Object}
	 */
	createElectronStateDisplayTextSection: function(charge, radical, partialChargeDecimalsLength, chargeMarkType, useAlterTripletRadicalMark)
	{
		var result = null;
		var slabel = '';
		/*
		var showCharge = (!!charge) && (!partialChargeDecimalsLength || (Math.abs(charge) > Math.pow(10, -partialChargeDecimalsLength)/2));
		if (showCharge)
		{
			var chargeSign = (charge > 0)? '+': '-';
			var chargeAmount = Math.abs(charge);
			if (chargeAmount != 1)
			{
				slabel += partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString();
			}
			else  // +1 or -1, may use different charge sign char
			{
				if (chargeMarkType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND)
					chargeSign = (charge > 0)? '\u2295': '\u2296';
			}
			slabel += chargeSign;
		}
		*/
		if (charge)
		{
			slabel = Kekule.Render.ChemDisplayTextUtils.getChargeDisplayText(charge, partialChargeDecimalsLength, chargeMarkType);
		}
		if (radical)
		{
			slabel += Kekule.Render.ChemDisplayTextUtils.getRadicalDisplayText(radical, useAlterTripletRadicalMark) || '';
		}

		if (slabel)
			result = Kekule.Render.RichTextUtils.createSection(slabel,
				{'textType': Kekule.Render.RichText.SUP, 'charDirection': Kekule.Render.TextDirection.LTR}
			);
		return result;
	},

	/**
	 * Convert a chemistry formula to a displayable rich format text label.
	 * @param {Kekule.MolecularFormula} formula
	 * @param {Bool} showCharge Whether display formula charge.
	 * @param {Bool} showRadical Whether display formula radical.
	 * @param {Int} partialChargeDecimalsLength
	 * @returns {Object}
	 */
	formulaToRichText: function(formula, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical)
	{
		//var result = Kekule.Render.RichTextUtils.create();
		var result = Kekule.Render.ChemDisplayTextUtils._convFormulaToRichTextGroup(formula, false, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical);
		return result;
	},

	/** @private */
	_convFormulaToRichTextGroup: function(formula, showBracket, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical)
	{
		var result = Kekule.Render.RichTextUtils.createGroup();
		var sections = formula.getSections();
		if (showBracket)
		{
			var bracketIndex = formula.getMaxNestedLevel() % Kekule.FormulaUtils.FORMULA_BRACKET_TYPE_COUNT;
			var bracketStart = Kekule.FormulaUtils.FORMULA_BRACKETS[bracketIndex][0];
			var bracketEnd = Kekule.FormulaUtils.FORMULA_BRACKETS[bracketIndex][1];
			result = Kekule.Render.RichTextUtils.appendText(result, bracketStart);
		}
		for (var i = 0, l = sections.length; i < l; ++i)
		{
			var obj = sections[i].obj;
			var charge = formula.getSectionCharge(sections[i]);
			var subgroup = null;
			if (obj instanceof Kekule.MolecularFormula)  // a sub-formula
			{
				// TODO: sometimes bracket is unessential, such as SO42- and so on, need more judge here
				subgroup = Kekule.Render.ChemDisplayTextUtils._convFormulaToRichTextGroup(obj, true, false, false, partialChargeDecimalsLength, displayConfigs, chargeMarkType); // do not show charge right after, we will add it later
			}
			else if (obj.getDisplayRichText) // an atom/isotope
			{
				var subgroup = obj.getDisplayRichText(Kekule.Render.HydrogenDisplayLevel.NONE, false, null, displayConfigs, partialChargeDecimalsLength, chargeMarkType);  // do not show charge right after symbol
			}

			if (subgroup)
			{
				// count
				if (sections[i].count != 1)
				{
					subgroup = Kekule.Render.RichTextUtils.appendText(subgroup, sections[i].count.toString(), {'textType': Kekule.Render.RichText.SUB});
					subgroup.charDirection = Kekule.Render.TextDirection.LTR;
				}

				// charge is draw after count
				if (showCharge && charge)
				{
					var chargeSection = Kekule.Render.ChemDisplayTextUtils.createElectronStateDisplayTextSection(charge, null, partialChargeDecimalsLength, chargeMarkType, distinguishSingletAndTripletRadical);
					if (chargeSection)
					{
						Kekule.Render.RichTextUtils.append(subgroup, chargeSection);
					}
				}

				result = Kekule.Render.RichTextUtils.append(result, subgroup);
			}
		}
		if (showBracket)
			result = Kekule.Render.RichTextUtils.appendText(result, bracketEnd);

		if (showCharge || showRadical)
		{
			var charge = formula.getCharge();
			var radical = formula.getRadical();
			var chargeSection = Kekule.Render.ChemDisplayTextUtils.createElectronStateDisplayTextSection(charge, radical, partialChargeDecimalsLength, chargeMarkType);
			if (chargeSection)
			{
				Kekule.Render.RichTextUtils.append(result, chargeSection);
			}
		}

		return result;
	}
};


/**
 * Help methods to draw text.
 * @class
 */
Kekule.Render.TextDrawUtils = {
	/**
	 * Check if a text line is in horizontal direction (LTR or RTL).
	 * @param {Object} charDirection
	 * @returns {Bool}
	 */
	isHorizontalLine: function(charDirection)
	{
		return ((charDirection == Kekule.Render.TextDirection.LTR)
			|| (charDirection == Kekule.Render.TextDirection.RTL)
			|| (charDirection == null));  // default is horizontal
	},

	/**
	 * Check if a text line is in vertical direction (TTB or BTT).
	 * @param {Object} charDirection
	 * @returns {Bool}
	 */
	isVerticalLine: function(charDirection)
	{
		return ((charDirection == Kekule.Render.TextDirection.TTB)
			|| (charDirection == Kekule.Render.TextDirection.BTT));
	},

	/**
	 * Get opposite direction.
	 * @param {Int} direction
	 * @returns {Int}
	 */
	getOppositeDirection: function(direction)
	{
		var TD = Kekule.Render.TextDirection;
		switch (direction)
		{
			case TD.LTR: return TD.RTL;
			case TD.RTL: return TD.LTR;
			case TD.TTB: return TD.BTT;
			case TD.BTT: return TD.TTB;
		}
	},
	/**
	 * Check if two directions are opposite.
	 * @param {Int} direction1
	 * @param {Int} direction2
	 * @return {Bool}
	 */
	isOppositeDirection: function(direction1, direction2)
	{
		return Kekule.Render.TextDrawUtils.getOppositeDirection(direction1) === direction2;
	},

	/**
	 * Turn line break direction to actual direction value.
	 * @param {Int} direction
	 * @param {Int} parentDirection
	 */
	getActualDirection: function(direction, parentDirection)
	{
		if (direction === TD.LINE_BREAK)
		{
			return ((parentDirection === TD.TTB) || (parentDirection === TD.BTT))? TD.LTR:
				((parentDirection === TD.LTR) || (parentDirection === TD.RTL))? TD.TTB:
				direction;
		}
		else
			return direction;
	},

	getActualAlign: function(align, direction)
	{
		var A = Kekule.Render.TextAlgin;
		var D = Kekule.Render.TextDirection;

		switch (align)
		{
			case A.LEFT:
			case A.RIGHT:
			case A.TOP:
			case A.BOTTOM:
			case A.CENTER:
			{
				return align;
				break;
			}
			case A.LEADING:
			{
				switch (direction)
				{
					case D.TTB: return A.TOP;
					case D.BTT: return A.BOTTOM;
					case D.RTL: return A.RIGHT;
					case D.LTR:
					default:
						return A.LEFT;
				}
			}
			case A.TRAILING:
			{
				switch (direction)
				{
					case D.BTT: return A.TOP;
					case D.TTB: return A.BOTTOM;
					case D.RTL: return A.LEFT;
					case D.LTR:
					default:
						return A.RIGHT;
				}
			}
		}
	}
}

/**
 * Help methods about path used in 2D renderer.
 * @class
 */
Kekule.Render.DrawPathUtils = {
	/**
	 * Make a path array from arguments.
	 * For instance, makePath('M', [10, 20], 'L', [20, 30]).
	 * The path method string is similiar to SVG path string format, including:
	 *  M	moveto	(x y)+
	 *  Z	closepath	(none)
	 * 	L	lineto	(x y)+
	 * 	//H	horizontal lineto	x+
	 * 	//V	vertical lineto	y+
	 * 	C	curveto	(x1 y1 x2 y2 x y)+
	 * 	S	smooth curveto	(x2 y2 x y)+
	 * 	Q	quadratic Bézier curveto	(x1 y1 x y)+
	 * 	T	smooth quadratic Bézier curveto	(x y)+
	 * 	A	elliptical arc	(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
	 */
	makePath: function()
	{
		var result = [];
		var sMethod = null;
		var params = [];
		var group;
		for (var i = 0, l = arguments.length; i < l; ++i)
		{
			var arg = arguments[i];
			if (DataType.isArrayValue(arg))
			{
				params = params.concat(arg);
			}
			else if (typeof(arg) === 'number')
			{
				params.push(arg);
			}
			else // start of a new method
 			{
				if (sMethod) // there is a prev method, group up it
				{
					group = {
						'method': sMethod,
						'params': params
					};
					result.push(group);
					// empty curr method and params
					sMethod = null;
					params = [];
				}
				// then new method
				sMethod = arg.toString();
			}

			if (i === l - 1)  // last one, wrap up last group
			{
				if (sMethod) // there is a prev method, group up it
				{
					group = {
						'method': sMethod,
						'params': params
					};
					result.push(group);
				}
			}
		}
		return result;
	}
};

/**
 * Help methods to draw connector (usually a bond).
 * @class
 */
Kekule.Render.ConnectorDrawUtils = {
	/**
	 * Get possible render type of a connector.
	 * If connector is a bond, this function will return the same result as {@link Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderType},
	 * otherwise a default set will be returned.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Array} A set of possible values from {@link Kekule.Render.BondRenderType}.
	 */
	getPossibleConnectorRenderTypes: function(connector)
	{
		if (connector instanceof Kekule.Bond)
			return Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderTypes(connector);
		else
		{
			var RT = Kekule.Render.BondRenderType;
			return [RT.SINGLE, RT.DASHED, RT.WAVY];
		}
	},
	/**
	 * Get default render type of a connector.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Int} Value from {@link Kekule.Render.BondRenderType}.
	 */
	getDefConnectorRenderType: function(connector)
	{
		var a = Kekule.Render.ConnectorDrawUtils.getPossibleConnectorRenderTypes(connector);
		return a[0];
	},
	/**
	 * Get possible render type of a bond.
	 * @param {Kekule.Bond} bond
	 * @returns {Array} A set of possible values from {@link Kekule.Render.BondRenderType}.
	 */
	getPossibleBondRenderTypes: function(bond)
	{
		var RT = Kekule.Render.BondRenderType;
		var BT = Kekule.BondType;
		var btype = bond.getBondType();
		switch (btype)
		{
			case BT.HYDROGEN: return [RT.DASHED, RT.ARROWED, RT.WAVY]; break;
			// TODO: Arrow direction of coordinate bond should be calculated
			case BT.COORDINATE: return [RT.ARROWED, RT.SINGLE, RT.DASHED, RT.WAVY]; break;
			case BT.IONIC: /*case BT.COORDINATE:*/ case BT.METALLIC: case BT.UNKNOWN:
				return [RT.SINGLE, RT.DASHED, RT.WAVY]; break;
			case BT.COVALENT: break;  // need further check in the following code
			default:
				return [RT.SINGLE, RT.DASHED, RT.ARROWED, RT.WAVY];
		}
		// if covalent bond, then further check it
		if (bond.getConnectedObjCount() > 2)  // multiple center bond
			return [RT.DASHED, RT.SINGLE];
		var BO = Kekule.BondOrder;
		switch (bond.getBondOrder())
		{
			case BO.DOUBLE:
			{
				var dresult = [RT.DOUBLE, RT.DASHED_DOUBLE, RT.BOLD_DOUBLE, RT.WAVY];
				if (bond.getStereo && [Kekule.BondStereo.E_OR_Z, Kekule.BondStereo.CIS_OR_TRANS].indexOf(bond.getStereo()) >= 0)
					dresult.unshift(RT.SCISSORS_DOUBLE);
				return dresult;
				break;
			}
			case BO.TRIPLE: return [RT.TRIPLE, RT.BOLD_TRIPLE, RT.WAVY]; break;
			case BO.QUAD: return [RT.QUAD, RT.BOLD_QUAD, RT.WAVY]; break;
			case BO.EXPLICIT_AROMATIC: return [RT.SOLID_DASH, RT.WAVY]; break;
			case BO.SINGLE:  // complex, need consider stereo chemistry
				{
					var BS = Kekule.BondStereo;
					var result = [RT.SINGLE, RT.BOLD, RT.WAVY];
					switch (bond.getStereo())
					{
						case BS.UP: result.unshift(RT.WEDGED_SOLID, RT.WEDGED_HOLLOW); break;
						case BS.UP_INVERTED: result.unshift(RT.WEDGED_SOLID_INV, RT.WEDGED_HOLLOW_INV); break;
						case BS.DOWN: result.unshift(RT.WEDGED_HASHED, RT.DASHED); break;
						case BS.DOWN_INVERTED: result.unshift(RT.WEDGED_HASHED_INV, RT.DASHED); break;
						case BS.UP_OR_DOWN: case BS.UP_OR_DOWN_INVERTED: result = [RT.WAVY, RT.SINGLE]; break;
						case BS.CLOSER: result = [RT.WEDGED_SOLID_BOTH, RT.WEDGED_HOLLOW_BOTH]; break;
						default:  // NONE
							;// do nothing
					}
					return result;
					break;
				}
			default: // OTHER, UNSET
				return [RT.SINGLE, RT.DASHED, RT.ARROWED, RT.WAVY];
		}
	},
	/**
	 * Get default render type of a bond.
	 * @param {Kekule.Bond} bond
	 * @returns {Int} Value from {@link Kekule.Render.BondRenderType}.
	 */
	getDefBondRenderType: function(bond)
	{
		var a = Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderTypes(bond);
		return a[0];
	},

	/**
	 * Get 3D render type of a connector.
	 * If connector is a bond, this function will return the same result as {@link Kekule.Render.ConnectorDrawUtils.getBondRender3DTypes},
	 * otherwise a default type will be returned.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @param {Int} renderMode Value from {@link Kekule.Render.Bond3DRenderMode}.
	 * @returns {Int} Value from {@link Kekule.Render.Bond3DRenderType}.
	 */
	getConnectorRender3DType: function(connector, renderMode)
	{
		var BRT = Kekule.Render.Bond3DRenderType;
		var BRM = Kekule.Render.Bond3DRenderMode;
		if (renderMode && ((renderMode === BRM.WIRE) || (renderMode === BRM.CYLINDER)))
			return BRT.SINGLE;
		else
		{
			if (connector instanceof Kekule.Bond)
				return Kekule.Render.ConnectorDrawUtils.getBondRender3DTypes(connector);
			else
				return BRT.SINGLE;
		}
	},
	/**
	 * Get suitable 3D render type of a bond.
	 * @param {Kekule.Bond} bond
	 * @returns {Int} Value from {@link Kekule.Render.Bond3DRenderType}.
	 */
	getBondRender3DTypes: function(bond)
	{
		var RT = Kekule.Render.Bond3DRenderType;
		var BT = Kekule.BondType;
		var btype = bond.getBondType();
		switch (btype)
		{
			case BT.HYDROGEN: return RT.DASH; break;
			case BT.COVALENT: break;  // need further check in the following code
			default:
				return RT.SINGLE;
		}
		// if covalent bond, then further check it
		var BO = Kekule.BondOrder;
		switch (bond.getBondOrder())
		{
			case BO.DOUBLE: return RT.DOUBLE; break;
			case BO.TRIPLE: return RT.TRIPLE; break;
			case BO.EXPLICIT_AROMATIC: return RT.SOLID_DASH; break;
			default: // OTHER, UNSET
				return RT.SINGLE;
		}
	}
};

/**
 * Help methods to manupilate chem object.
 * @class
 */
Kekule.Render.ObjUtils = {
	/**
	 * Returns 2D containing box of usual chem object.
	 * @param {Kekule.ChemObjecy} chemObj
	 * @param {Int} coordMode
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash}
	 */
	getContainerBox: function(chemObj, coordMode, allowCoordBorrow)
	{
		if (!coordMode)
			coordMode = Kekule.CoordMode.COORD2D;
		var box;
		var o = chemObj;
		/*
		if (o.getExposedContainerBox2D)
			box = o.getExposedContainerBox2D(allowCoordBorrow);
		else if (o.getContainerBox2D)
			box = o.getContainerBox2D(allowCoordBorrow);
		else  // no containerBox related method, use coord
			box = null;
		*/
		if (o.getExposedContainerBox)
			box = o.getExposedContainerBox(coordMode, allowCoordBorrow);
		else if (o.getContainerBox)
			box = o.getContainerBox(coordMode, allowCoordBorrow);
		else  // no containerBox related method, use coord
		{
			var coord = o.getAbsCoordOfMode? o.getAbsCoordOfMode(coordMode): null;
			box = coord? Kekule.BoxUtils.createBox(coord, coord): null;
		}

		return box;
	}
};

/**
 * Help methods to manipulate bound info base on simple shape.
 * @class
 */
Kekule.Render.MetaShapeUtils = {
//Kekule.Render.BoundUtils = {
	/**
	 * Create a new ShapeInfo object.
	 * @param {Int} shapeType
	 * @param {Array} coords
	 * @param {Hash} additionalInfos
	 * @returns {Object}
	 */
	createShapeInfo: function(shapeType, coords, additionalInfos)
	//createBoundInfo: function(boundType, coords, additionalInfos)
	{
		var result = {'shapeType': shapeType, 'coords': coords};
		/*
		result.shapeType = shapeType;
		result.coords = coords;
		*/
		if (additionalInfos)
			result = Object.extend(result, additionalInfos);
		return result;
	},
	/**
	 * Check if shape is a composite one (usually is an array of simple shapes).
	 * @param {Variant} shape
	 * @returns {Bool}
	 */
	isCompositeShape: function(shape)
	{
		return DataType.isArrayValue(shape);
	},
	/**
	 * Inflate shape with delta on each direction.
	 * @param {Object} originalShape
	 * @param {Float} delta
	 * @returns {Object} A new boundInfo.
	 */
	inflateShape: function(originalShape, delta)
	{
		if (!originalShape)
			return null;
		var T = Kekule.Render.MetaShapeType;
		var B = Kekule.Render.MetaShapeUtils;

		// composite shape
		if (B.isCompositeShape(originalShape))
		{
			var result = [];
			for (var i = 0, l = originalShape.length; i < l; ++i)
			{
				var oldChildShape = originalShape[i];
				var newChildShape = B.inflateShape(oldChildShape, delta);
				result.push(newChildShape);
			}
			return result;
		}

		// simple shape
		var newBound;
		switch (originalShape.shapeType)
		{
			case T.POINT: newBound = B._inflatePointShape(originalShape, delta); break;
			case T.CIRCLE: newBound = B._inflateCircleShape(originalShape, delta); break;
			case T.LINE: newBound = B._inflateLineShape(originalShape, delta); break;
			case T.RECT: newBound = B._inflateRectShape(originalShape, delta); break;
			case T.POLYGON: newBound = B._inflatePolygonShape(originalShape, delta); break;
			case T.ARC: newBound = B._inflateArcShape(originalShape, delta); break;
		}
		return newBound;
	},
	/** @private */
	_inflatePointShape: function(originalShape, delta)
	{
		var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
			Kekule.Render.BoundShapeType.CIRCLE, [originalShape.coords[0]], {'radius': delta});
		return newBound;
	},
	/** @private */
	_inflateCircleShape: function(originalShape, delta)
	{
		var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
			Kekule.Render.BoundShapeType.CIRCLE, [originalShape.coords[0]], {'radius': originalShape.radius + delta});
		return newBound;
	},
	/** @private*/
	_inflateLineShape: function(originalShape, delta)
	{
		var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
			Kekule.Render.BoundShapeType.LINE,
			[originalShape.coords[0], originalShape.coords[1]], {'width': (originalShape.width || 0) + delta * 2});
		return newBound;
	},
	/** @private*/
	_inflateArcShape: function(originalShape, delta)
	{
		var newBound = Object.extend({}, originalShape);
		newBound.width = (originalShape.width || 0) + delta * 2;
		return newBound;
	},
	/** @private */
	_inflateRectShape: function(originalShape, delta)
	{
		var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(Kekule.Render.BoundShapeType.RECT, []);
		var newCoords = Kekule.Render.MetaShapeUtils._inflateCoords(originalShape.coords, delta);
		newBound.coords = newCoords;
		return newBound;
	},
	/** @private */
	_inflatePolygonShape: function(originalShape, delta)
	{
		var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(Kekule.Render.BoundShapeType.POLYGON, []);
		var newCoords = Kekule.Render.MetaShapeUtils._inflateCoords(originalShape.coords, delta);
		newBound.coords = newCoords;
		return newBound;
	},
	/** @private */
	_inflateCoords: function(coords, delta)
	{
		var C = Kekule.CoordUtils;
		var result = [];
		// calc center of coords
		var center = C.getCenter(coords);
		for (var i = 0, l = coords.length; i < l; ++i)
		{
			var coord = coords[i];
			var vec = C.substract(coord, center);
			var distance = C.getDistance(coord);
			var deltaVec = C.multiply(vec, delta / distance);
			var newCoord = C.add(coord, deltaVec);
			result.push(newCoord);
		}
		return result;
	},
	/**
	 * Returns distance of coord to a bound shape. A negative value means coord insude shape.
	 * @param {Hash} coord
	 * @param {Hash} shapeInfo Provides the shape box information of this object on context. It has the following fields:
	 *   {
	 *     shapeType: value from {@link Kekule.Render.MetaShapeType}.
	 *     coords: [Array of coords]
	 *     otherInfo: ...
	 *   }
	 *   Note that shapeInfo may be an array, in that case, nearest distance will be returned.
	 * @param {Float} inflate
	 * @returns {Float}
	 */
	getDistance: function(coord, shapeInfo, inflate)
	{
		if (Kekule.Render.MetaShapeUtils.isCompositeShape(shapeInfo))
		{
			var result = null;
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var info = shapeInfo[i];
				var d = Kekule.Render.MetaShapeUtils.getDistance(coord, info, inflate);
				if (result === null || result > d)
					result = d;
			}
			return result;
		}
		else
		{
			var T = Kekule.Render.MetaShapeType;
			var B = Kekule.Render.MetaShapeUtils;
			var newBound = inflate? B.inflateShape(shapeInfo, inflate): shapeInfo;
			switch (shapeInfo.shapeType)
			{
				case T.POINT: return (inflate? B._getDistanceToCircle(coord, newBound): B._getDistanceToPoint(coord, newBound));
				case T.CIRCLE: return B._getDistanceToCircle(coord, newBound);
				case T.LINE: return B._getDistanceToLine(coord, newBound);
				case T.RECT: return B._getDistanceToRect(coord, newBound);
				case T.POLYGON: return B._getDistanceToPolygon(coord, newBound);
				case T.ARC: return B._getDistanceToArc(coord, newBound);
				default: return false;
			}
		}
	},
	/** @private */
	_getDistanceToPoint: function(coord, shapeInfo)
	{
		return Kekule.CoordUtils.getDistance(coord, shapeInfo.coords[0]);
	},
	/** @private */
	_getDistanceToCircle: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var d = C.getDistance(coord, shapeInfo.coords[0]);
		return d - shapeInfo.radius;
	},
	/** @private */
	_getDistanceToArc: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var radiusVector = C.substract(coord, shapeInfo.coords[0]);
		var currAngle = Math.atan2(radiusVector.y, radiusVector.x);
		if (Kekule.Render.MetaShapeUtils.isAngleInArcRange(currAngle, shapeInfo.startAngle, shapeInfo.endAngle, shapeInfo.anticlockwise))
		{
			// coord in arc range, check distance to arc
			var d = C.getDistance(coord, shapeInfo.coords[0]);
			var d = Math.abs(d - shapeInfo.radius);
			if (shapeInfo.width && shapeInfo.width > 1)
				d = d - shapeInfo.width / 2;
			return Math.max(0, d);
		}
		else  // outside arc range, check the min distance to two end points
		{
			var centerCoord = shapeInfo.coords[0];
			var rs;
			if (shapeInfo.width && shapeInfo.width > 1)
			{
				rs = [shapeInfo.radius - shapeInfo.width / 2, shapeInfo.radius + shapeInfo.width / 2];
			}
			else
			{
				rs = [shapeInfo.radius];
			}

			var testPoints = [];
			var startAngle = shapeInfo.startAngle;
			var endAngle = shapeInfo.endAngle;
			for (var i = 0, l = rs.length; i <l; ++i)
			{
				var r = rs[i];
				var startCoord = CU.add(centerCoord, {'x': r * Math.cos(startAngle), 'y': r * Math.sin(startAngle)});
				var endCoord = CU.add(centerCoord, {'x': r * Math.cos(endAngle), 'y': r * Math.sin(endAngle)});
				testPoints.push(startCoord);
				testPoints.push(endCoord);
			}
			var result = null;
			for (var i = 0, l = testPoints.length; i < l; ++i)
			{
				var d = C.getDistance(testPoints[i], coord);
				if (result === null || result > d)
					result = d;
			}
			return result;
		}
	},
	/** @private */
	_getDistanceToLine: function(coord, shapeInfo)
	{
		var SU = Kekule.Render.MetaShapeUtils;
		// get cross point of coord and line
		var lineCoords = shapeInfo.coords;
		var crossPoint = Kekule.Render.MetaShapeUtils._calcVerticalLineCrossPoint(coord, lineCoords[0], lineCoords[1]);
		// check if cross point is between two coords of line
		var betweenFlag;

		var NU = Kekule.NumUtils;
		//if (lineCoords[0].x !== lineCoords[1].x)
		/*
		 if (!NU.isFloatEqual(lineCoords[0].x, lineCoords[1].x))
		 betweenFlag = (Math.sign(crossPoint.x - lineCoords[0].x) * Math.sign(crossPoint.x - lineCoords[1].x) <= 0);
		 //else if (lineCoords[0].y !== lineCoords[1].y)
		 else if (!NU.isFloatEqual(lineCoords[0].y, lineCoords[1].y))
		 betweenFlag = (Math.sign(crossPoint.y - lineCoords[0].y) * Math.sign(crossPoint.y - lineCoords[1].y) <= 0);
		 else // x/y of coord0/1 all equal, two coords are actually the same point
		 */
		if (NU.isFloatEqual(lineCoords[0].x, lineCoords[1].x) && NU.isFloatEqual(lineCoords[0].y, lineCoords[1].y))
		// x/y of coord0/1 all equal, two coords are actually the same point
		{
			var circleInfo = {coords: [shapeInfo.coords[0]], radius: shapeInfo.width};
			var result = SU._getDistanceToCircle(coord, circleInfo);
			return result;
		}
		else
		{
			var vector = Kekule.CoordUtils.substract(lineCoords[1], lineCoords[0]);
			if (Math.abs(vector.y) > Math.abs(vector.x))
				betweenFlag = (Math.sign(crossPoint.y - lineCoords[0].y) * Math.sign(crossPoint.y - lineCoords[1].y) <= 0);
			else
				betweenFlag = (Math.sign(crossPoint.x - lineCoords[0].x) * Math.sign(crossPoint.x - lineCoords[1].x) <= 0);
		}

		if (!betweenFlag)  // not inside
		{
			//console.log(crossPoint, crossPoint.x - lineCoords[0].x, crossPoint.x - lineCoords[1].x);
			//return false;
			// returns distance to nearest end point circle
			var circleInfo0 = {coords: [shapeInfo.coords[0]], radius: shapeInfo.width || 0};
			var circleInfo1 = {coords: [shapeInfo.coords[1]], radius: shapeInfo.width || 0};
			var d0 = SU._getDistanceToCircle(coord, circleInfo0);
			var d1 = SU._getDistanceToCircle(coord, circleInfo1);
			return Math.min(d0, d1);
		}
		else  // inside line (with width)
		{
			var distance = Kekule.CoordUtils.getDistance(coord, crossPoint) - (shapeInfo.width || 0) / 2;
			//console.log('distance', distance, boundInfo.width);
			//return (distance <= shapeInfo.width / 2);
			return distance;
		}
	},
	/** @private */
	_getDistanceToRect: function(coord, shapeInfo)
	{
		var cornerCoords = shapeInfo.coords;
		var dx0 = (coord.x - cornerCoords[0].x);
		var dx1 = (coord.x - cornerCoords[1].x);
		var dy0 = (coord.y - cornerCoords[0].y);
		var dy1 = (coord.y - cornerCoords[1].y);
		var inside = (dx0 * dx1 <= 0) && (dy0 * dy1 <= 0);
		if (inside)
			return -Math.min(Math.abs(dx0), Math.abs(dx1), Math.abs(dy0), Math.abs(dy1));
		else  // outside, calc distance to each corner
		{
			var CU = Kekule.CoordUtils;
			return Math.min(
					CU.getDistance(coord, cornerCoords[0]),
					CU.getDistance(coord, cornerCoords[1]),
					CU.getDistance(coord, {'x': cornerCoords[0].x, 'y': cornerCoords[1].y}),
					CU.getDistance(coord, {'x': cornerCoords[1].x, 'y': cornerCoords[0].y})
			);
		}
	},
	/** @private */
	_getDistanceToPolygon: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var SU = Kekule.Render.MetaShapeUtils;
		var T = Kekule.Render.MetaShapeType;
		var inside = SU._isInsidePolygon(coord, shapeInfo);
		var lineCoords = shapeInfo.coords;
		var distance = null;
		for (var i = 0, l = lineCoords.length; i < l; ++i)
		{
			var coord1 = lineCoords[i];
			var coord2 = lineCoords[(i + 1) % l];
			// calc distance to line
			var lineShape = SU.createShapeInfo(T.LINE, [coord1, coord2], {'width': 0});
			var d = SU._getDistanceToLine(coord, lineShape);
			if (distance === null)
				distance = d;
			else if (distance > d)
				distance = d;
		}
		if (inside)
			distance = -distance;
		return distance;
	},
	/** @private */
	_getDistanceOfTwoLines: function(lineCoords1, lineCoords2)
	{
		var U = Kekule.Render.MetaShapeUtils;
		var GU = Kekule.GeometryUtils;
		// distance is 0 if two line segments cross
		if (GU.getCrossPointOfLines(lineCoords1[0], lineCoords1[1], lineCoords2[0], lineCoords2[1]))
			return 0;
		var d1 = GU.getDistanceFromPointToLine(lineCoords1[0], lineCoords2[0], lineCoords2[1]);
		var d2 = GU.getDistanceFromPointToLine(lineCoords1[1], lineCoords2[0], lineCoords2[1]);
		var d3 = GU.getDistanceFromPointToLine(lineCoords2[0], lineCoords1[0], lineCoords1[1]);
		var d4 = GU.getDistanceFromPointToLine(lineCoords2[1], lineCoords1[0], lineCoords1[1]);
		return Math.min(d1, d2, d3, d4);
	},

	/**
	 * Check if a point is inside a bound.
	 * @param {Hash} coord
	 * @param {Hash} shapeInfo Provides the shape box information of this object on context. It has the following fields:
	 *   {
	 *     shapeType: value from {@link Kekule.Render.MetaShapeType}.
	 *     coords: [Array of coords]
	 *     otherInfo: ...
	 *   }
	 *   Note that shapeInfo may be an array to check if coord in either of items of array.
	 * @param {Float} inflate
	 * @returns {Bool}
	 */
	isCoordInside: function(coord, shapeInfo, inflate)
	{
		if (!coord)
			return false;
		//if (Kekule.ArrayUtils.isArray(shapeInfo))
		if (Kekule.Render.MetaShapeUtils.isCompositeShape(shapeInfo))
		{
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var info = shapeInfo[i];
				if (Kekule.Render.MetaShapeUtils.isCoordInside(coord, info, inflate))
					return true;
			}
			return false;
		}
		else
		{
			var T = Kekule.Render.MetaShapeType;
			var B = Kekule.Render.MetaShapeUtils;
			var newBound = inflate? B.inflateShape(shapeInfo, inflate): shapeInfo;
			if (!newBound)
				return false;
			switch (shapeInfo.shapeType)
			{
				case T.POINT: return (inflate? B._isInsideCircle(coord, newBound): B._isInsidePoint(coord, newBound));
				case T.CIRCLE: return B._isInsideCircle(coord, newBound);
				case T.LINE: return B._isInsideLine(coord, newBound);
				case T.RECT: return B._isInsideRect(coord, newBound);
				case T.POLYGON: return B._isInsidePolygon(coord, newBound);
				case T.ARC: return B._isInsideArc(coord, newBound);
				default: return false;
			}
		}
	},
	/** @private */
	_isInsidePoint: function(coord, shapeInfo)
	{
		return Kekule.CoordUtils.isEqual(coord, shapeInfo.coords[0]);
	},
	/** @private */
	_isInsideCircle: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var d = C.getDistance(coord, shapeInfo.coords[0]);
		return (d <= shapeInfo.radius);
	},
	/** @private */
	_isInsideLine: function(coord, shapeInfo)
	{
		// get cross point of coord and line
		var lineCoords = shapeInfo.coords;
		var crossPoint = Kekule.Render.MetaShapeUtils._calcVerticalLineCrossPoint(coord, lineCoords[0], lineCoords[1]);
		//console.log(crossPoint);
		// check if cross point is between two coords of line
		var betweenFlag;

		var NU = Kekule.NumUtils;
		//if (lineCoords[0].x !== lineCoords[1].x)
		/*
		if (!NU.isFloatEqual(lineCoords[0].x, lineCoords[1].x))
			betweenFlag = (Math.sign(crossPoint.x - lineCoords[0].x) * Math.sign(crossPoint.x - lineCoords[1].x) <= 0);
		//else if (lineCoords[0].y !== lineCoords[1].y)
		else if (!NU.isFloatEqual(lineCoords[0].y, lineCoords[1].y))
			betweenFlag = (Math.sign(crossPoint.y - lineCoords[0].y) * Math.sign(crossPoint.y - lineCoords[1].y) <= 0);
		else // x/y of coord0/1 all equal, two coords are actually the same point
		*/
		if (NU.isFloatEqual(lineCoords[0].x, lineCoords[1].x) && NU.isFloatEqual(lineCoords[0].y, lineCoords[1].y))
			// x/y of coord0/1 all equal, two coords are actually the same point
		{
			var circleInfo = {coords: [shapeInfo.coords[0]], radius: shapeInfo.width};
			var result = Kekule.Render.MetaShapeUtils._isInsideCircle(coord, circleInfo);
			return result;
		}
		else
		{
			var vector = Kekule.CoordUtils.substract(lineCoords[1], lineCoords[0]);
			if (Math.abs(vector.y) > Math.abs(vector.x))
				betweenFlag = (Math.sign(crossPoint.y - lineCoords[0].y) * Math.sign(crossPoint.y - lineCoords[1].y) <= 0);
			else
				betweenFlag = (Math.sign(crossPoint.x - lineCoords[0].x) * Math.sign(crossPoint.x - lineCoords[1].x) <= 0);
		}

		if (!betweenFlag)  // not inside
		{
			//console.log(crossPoint, crossPoint.x - lineCoords[0].x, crossPoint.x - lineCoords[1].x);
			return false;
		}
		else
		{
			var distance = Kekule.CoordUtils.getDistance(coord, crossPoint);
			//console.log('distance', distance, boundInfo.width);
			return (distance <= shapeInfo.width / 2);
		}
	},
	/** @private */
	_isInsideArc: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var G = Kekule.GeometryUtils;
		var d = C.getDistance(coord, shapeInfo.coords[0]);
		var result = (Math.abs(d - shapeInfo.radius) < shapeInfo.width / 2);  // check if in arc circle
		if (result)  // if true, further check the position if on the angle range of arc
		{
			var radiusVector = C.substract(coord, shapeInfo.coords[0]);
			var currAngle = G.standardizeAngle(Math.atan2(radiusVector.y, radiusVector.x), 0);
			/*
			var startAngle = G.standardizeAngle(shapeInfo.startAngle);
			var endAngle = G.standardizeAngle(shapeInfo.endAngle);
			var sign = Math.sign(currAngle - startAngle) * Math.sign(currAngle - endAngle) * Math.sign(endAngle - startAngle);
			result = shapeInfo.anticlockwise? (sign >= 0): sign <= 0;
			*/
			result = Kekule.Render.MetaShapeUtils.isAngleInArcRange(currAngle, shapeInfo.startAngle, shapeInfo.endAngle, shapeInfo.anticlockwise);
		}
		return result;
	},
	/** @private */
	_isInsideRect: function(coord, shapeInfo)
	{
		var lineCoords = shapeInfo.coords;
		/*
		return ((coord.x - lineCoords[0].x) * (coord.x - lineCoords[1].x) <= 0)
			&& ((coord.y - lineCoords[0].y) * (coord.y - lineCoords[1].y) <= 0);
		*/
		return Kekule.CoordUtils.insideRect(coord, lineCoords[0], lineCoords[1]);
	},
	/** @private */
	_isInsidePolygon: function(coord, shapeInfo)
	{
		var C = Kekule.CoordUtils;
		var lineCoords = shapeInfo.coords;
		var crossCount = 0;
		for (var i = 0, l = lineCoords.length; i < l; ++i)
		{
			var coord1 = lineCoords[i];
			var coord2 = lineCoords[(i + 1) % l];
			// calc cross point of line x = coord.x and line (coord1 - coord2)
			var deltaVec = C.substract(coord2, coord1);
			if (deltaVec.x === 0)  // line (coord1 - coord2) is a vertical line, will not cross
				continue;
			else
			{
				var crossCoord = {'x': coord.x};
				crossCoord.y = ((crossCoord.x - coord1.x) / deltaVec.x) * deltaVec.y + coord1.y;
				if (C.insideRect(crossCoord, coord1, coord2))
					++crossCount;
			}
		}
		return (crossCount % 2 === 0);
	},
	/**
	 * Calc cross point of coord to line (lineCoord1 - lineCoord2).
	 * @private
	 */
	_calcVerticalLineCrossPoint: function(coord, lineCoord1, lineCoord2)
	{
		/*
		// methods are from http://blog.csdn.net/fly542/article/details/6638299
		// first turn line formula to Ax + By + C = 0 form
		var A = (lineCoord1.y - lineCoord2.y) / (lineCoord1.x - lineCoord2.x);
		var B = lineCoord1.y - A * lineCoord1.y;
		// its vertical line formula is Bx - Ay + m = 0, calc m
		var m = coord.x - A * coord.y;
		// calc cross point coord
		var result = {};
		result.x = (m - A * B) / (A * A + 1);
		result.y = A * result.x + B;
		return result;
		*/
		// method from http://blog.sina.com.cn/s/blog_4bf793ad0100gudn.html
		var result = {};
		if (Math.abs(lineCoord1.x - lineCoord2.x) < 1e-10)  // a vertical line or near vertical line
		{
			result.x = lineCoord1.x;
			result.y = coord.y;
		}
		else
		{
			var k = (lineCoord2.y - lineCoord1.y) / (lineCoord2.x - lineCoord1.x);
			// 垂线的斜率为 - 1 / k,垂线方程为:y = (-1/k) * (x - coord.x) + coord.y
			// calc cross point
			// x = ( k^2 * pt1.x + k * (point.y - pt1.y ) + point.x ) / ( k^2 + 1) ,y = k * ( x - pt1.x) + pt1.y
			result.x = (k * k * lineCoord1.x + k * (coord.y - lineCoord1.y) + coord.x) / (k * k + 1);
			result.y = k * (result.x - lineCoord1.x) + lineCoord1.y;
		}
		return result;
	},

	/**
	 * Returns the minimum rect that contains this shape.
	 * @param {Object} shapeInfo
	 * @param {Float} Inflation
	 * returns {Object}
	 */
	getContainerBox: function(shapeInfo, inflation)
	{
		var T = Kekule.Render.MetaShapeType;
		var U = Kekule.Render.MetaShapeUtils;
		var C = Kekule.BoxUtils;

		if (!shapeInfo)
			return null;

		var coords = shapeInfo.coords;
		inflation = inflation || 0;
		var result;

		if (U.isCompositeShape(shapeInfo))  // composite shape
		{
			result = null;
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var childShape = shapeInfo[i];
				var childBox = U.getContainerBox(childShape);
				if (!result)
					result = childBox;
				else
					result = C.getContainerBox(result, childBox);
			}
		}
		else  // simple shape
		{
			switch (shapeInfo.shapeType)
			{
				case T.POINT:
				case T.CIRCLE:
					{
						var radius = (shapeInfo.shapeType === T.POINT)? 0: (shapeInfo.radius || 0);
						var coord = coords[0];
						result = C.createBox({x: coord.x - radius, y: coord.y - radius}, {x: coord.x + radius, y: coord.y + radius});
						break;
					}
				case T.LINE:
					{
						result = C.createBox(coords[0], coords[1]);
						// TODO: a rough calculate
						result = Kekule.BoxUtils.inflateBox(result, (shapeInfo.width || 0) / 2);
						break;
					}
				case T.ARC:
					{
						result = Kekule.Render.MetaShapeUtils._getArcContainerBox(shapeInfo);
						break;
					}
				case T.RECT:
					{
						// TODO: does not consider line width here
						result = C.createBox(coords[0], coords[1]);
						break;
					}
				case T.POLYGON:
					{
						result = C.createBox(coords[0], coords[0]);
						for (var i = 1, l = coords.length; i < l; ++i)
						{
							var coord = coords[i];
							result = {
								'x1': Math.min(result.x1, coord.x),
								'y1': Math.min(result.y1, coord.y),
								'x2': Math.max(result.x2, coord.x),
								'y2': Math.max(result.y2, coord.y)
							};
						}
						break;
					}
			}
		}
		if (inflation)
		{
			result = Kekule.BoxUtils.inflateBox(result, inflation);
		}
		return result;
	},
	/** @private */
	_getArcContainerBox: function(shapeInfo)
	{
		var U = Kekule.Render.MetaShapeUtils;
		var CU = Kekule.CoordUtils;

		var radius;
		if (shapeInfo.width && shapeInfo.width > 1)
		{
			radius = [shapeInfo.radius + shapeInfo.width / 2, shapeInfo.radius - shapeInfo.width / 2];
		}
		else
		{
			radius = [shapeInfo.radius];
		}
		var centerCoord = shapeInfo.coords[0];
		var degree90 = Math.PI / 2;

		var getCoordOfAngle = function(angle, radius)
		{
			return CU.add(centerCoord, {'x': radius * Math.cos(angle), 'y': radius * Math.sin(angle)});
		};

		var anchorPoints = [];

		for (var i = 0, l = radius.length; i < l; ++i)
		{
			var currRadius = radius[i];
			var startPoint = getCoordOfAngle(shapeInfo.startAngle, currRadius);
			var endPoint = getCoordOfAngle(shapeInfo.endAngle, currRadius);
			anchorPoints = anchorPoints.concat([startPoint, endPoint]);

			var testAngles = [0, degree90, degree90 * 2, degree90 * 3];
			for (var j = 0, k = testAngles.length; j < k; ++j)
			{
				if (U.isAngleInArcRange(testAngles[j], shapeInfo.startAngle, shapeInfo.endAngle, shapeInfo.anticlockwise))
				{
					anchorPoints.push(getCoordOfAngle(testAngles[j], currRadius));
				}
			}
		}

		return CU.getContainerBox(anchorPoints);
	},

	/**
	 * Check if testAngle is in the arc sector.
	 * @param {Float} testAngle
	 * @param {Float} startAngle
	 * @param {Float} endAngle
	 * @param {Bool} anticlockwise
	 * @returns {Bool}
	 */
	isAngleInArcRange: function(testAngle, startAngle, endAngle, anticlockwise)
	{
		var s = Kekule.GeometryUtils.standardizeAngle;
		var aS = s(startAngle);
		var aE = s(endAngle);
		var aT = s(testAngle);
		var sign = Math.sign(aT - aS) * Math.sign(aT - aE) * Math.sign(aE - aS);
		return anticlockwise? (sign >= 0): sign <= 0;
	},

	/**
	 * Check if a shape is intersecting with a line with a certain stroke width.
	 * @param {Object} shapeInfo
	 * @param {Array} lineCoords
	 * @param {Number} lineWidth
	 * @returns {Bool}
	 */
	isIntersectingLine: function(shapeInfo, lineCoords, lineWidth)
	{
		var T = Kekule.Render.MetaShapeType;
		var U = Kekule.Render.MetaShapeUtils;

		if (U.isCompositeShape(shapeInfo))
		{
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var childShape = shapeInfo[i];
				if (U.isIntersectingLine(childShape, lineCoords, lineWidth))
					return true;
			}
			return false;
		}
		else
		{
			var lineShape = U.createShapeInfo(T.LINE, lineCoords);
			var halfWidth = lineWidth / 2;
			var shapeCoords = shapeInfo.coords;
			switch (shapeInfo.shapeType)
			{
				case T.POINT:
				{
					var d = U._getDistanceToLine(shapeCoords[0], lineShape);
					return d <= halfWidth;
				}
				case T.CIRCLE:
				{
					var d = U._getDistanceToLine(shapeCoords[0], lineShape);
					return d <= halfWidth + (lineShape.radius || 0);
				}
				case T.LINE:
				{
					var d = U._getDistanceOfTwoLines(shapeCoords, lineCoords);
					return d <= halfWidth + (lineShape.width || 0);
				}
				case T.RECT:
				case T.POLYGON:
				{
					var coords = U._getCoordsForShapeInComparingWithPolygon(shapeInfo);
					// calc min distance of each polygon edge to line
					var d = null;
					var j = coords.length - 1;
					for (var i = 0, l = coords.length; i < l; ++i)
					{
						var edgeCoords = [coords[i], coords[j]];
						var currDistance = U._getDistanceOfTwoLines(edgeCoords, lineCoords);
						if (d === null || currDistance < d)
							d = currDistance;
						j = i;
					}
					return d <= halfWidth;
				}
			}
			return false;
		}
	},
	/**
	 * Check if a shape is inside a rect box.
	 * @param {Object} shapeInfo
	 * @param {Hash} box
	 * @returns {Bool}
	 */
	isInsideBox: function(shapeInfo, box)
	{
		var cbox = Kekule.Render.MetaShapeUtils.getContainerBox(shapeInfo);
		return Kekule.BoxUtils.isInside(cbox, box);
	},
	/**
	 * Check if a shape is intersecting with box.
	 * @param {Object} shapeInfo
	 * @param {Hash} box
	 * @returns {Bool}
	 */
	isIntersectingBox: function(shapeInfo, box)
	{
		if (U.isCompositeShape(shapeInfo))
		{
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var childShape = shapeInfo[i];
				var childInside = Kekule.Render.MetaShapeUtils.isIntersectingBox(childShape, box);
				if (childInside)
					return true;
			}
		}
		else
		{
			// TODO: currently only check coords, line width or circle radius are not considered
			var coords = shapeInfo.coords;
			for (var i = 0, l = coords.length; i < l; ++i)
			{
				var coord = coords[i];
				if (Kekule.CoordUtils.isInsideBox(coord, box))
					return true;
			}
			return false;
		}
	},
	/**
	 * Check if a 2D point inside a polygon.
	 * The algorithm is from https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
	 * @param {Hash} pointCoord
	 * @param {Array} polygonCoords
	 */
	isPointInsidePolygon: function(pointCoord, polygonCoords)
	{
		var result = false;
		var length = polygonCoords.length;
		var j = length - 1;
		for (var i = 0; i < length; ++i)
		{
			if (((polygonCoords[i].y > pointCoord.y) !== (polygonCoords[j].y > pointCoord.y)) &&
					(pointCoord.x < (polygonCoords[j].x - polygonCoords[i].x) * (pointCoord.y - polygonCoords[i].y) / (polygonCoords[j].y - polygonCoords[i].y) + polygonCoords[i].x))
			  result = !result;
			j = i;
		}
		return result;
	},
	/**
	 * Check if a shape is inside a polygon defined by polygonCoords.
	 * @param {Object} shapeInfo
	 * @param {Array} polygonCoords
	 * @returns {Bool}
	 */
	isInsidePolygon: function(shapeInfo, polygonCoords)
	{
		var U = Kekule.Render.MetaShapeUtils;
		var testCoords = U._getCoordsForShapeInComparingWithPolygon(shapeInfo);
		if (!testCoords)
			return false;
		// every test coords should be in polygon
		for (var i = 0, l = testCoords.length; i < l; ++i)
		{
			var c = testCoords[i];
			if (!U.isPointInsidePolygon(c, polygonCoords))
				return false;
		}
		return true;
	},
	/**
	 * Check if a shape is intersecting with a polygon defined by polygonCoords.
	 * @param {Object} shapeInfo
	 * @param {Array} polygonCoords
	 * @returns {Bool}
	 */
	isIntersectingPolygon: function(shapeInfo, polygonCoords)
	{
		var U = Kekule.Render.MetaShapeUtils;
		var testCoords = U._getCoordsForShapeInComparingWithPolygon(shapeInfo);
		if (!testCoords)
			return false;
		// at least one test coord should be in polygon
		for (var i = 0, l = testCoords.length; i < l; ++i)
		{
			var c = testCoords[i];
			if (U.isPointInsidePolygon(c, polygonCoords))
				return true;
		}
		return false;
	},
	/**
	 * Check if a shape is intersecting with a polyline with a certain stroke width.
	 * @param {Object} shapeInfo
	 * @param {Array} lineCoords
	 * @param {Number} lineWidth
	 * @returns {Bool}
	 */
	isIntersectingPolyline: function(shapeInfo, polylineCoords, lineWidth)
	{
		var U = Kekule.Render.MetaShapeUtils;
		for (var i = 0, l = polylineCoords.length - 1; i < l; ++i)
		{
			var lineCoords = [polylineCoords[i], polylineCoords[i + 1]];
			if (U.isIntersectingLine(shapeInfo, lineCoords, lineWidth))
				return true;
		}
		return false;
	},
	/** @private */
	_getCoordsForShapeInComparingWithPolygon: function(shapeInfo)
	{
		var T = Kekule.Render.MetaShapeType;
		var U = Kekule.Render.MetaShapeUtils;

		var result;
		if (U.isCompositeShape(shapeInfo))
		{
			result = [];
			for (var i = 0, l = shapeInfo.length; i < l; ++i)
			{
				var childShape = shapeInfo[i];
				var childCoords = U._getCoordsForShapeInComparingWithPolygon(childShape);
				result = result.concat(childCoords);
			}
			return result;
		}
		else
		{
			var coords = shapeInfo.coords;
			switch (shapeInfo.shapeType)
			{
				case T.POINT:
				{
					result = [coords[0]];
					break;
				}
				case T.CIRCLE:
				{
					var radius = shapeInfo.radius || 0;
					if (!radius)
						result = [coords[0]];
					else // create a octagon to simulate the circle
					{
						var r = radius;
						var a = r / Math.sqrt(2);
						result = [{x: r, y: 0}, {x: a, y: a}, {x: 0, y: r}, {x: -a, y: a}, {x: -r, y: 0}, {x: -a, y: -a}, {
							x: 0,
							y: -r
						}, {x: a, y: -a}];
					}
					break;
				}
				case T.LINE:
				{
					result = [coords[0], coords[1]];
					break;
				}
				case T.RECT:
				{
					var c0 = coords[0], c1 = coords[1];
					result = [c0, {x: c1.x, y: c0.y}, c1, {x: c0.x, y: c1.y}];
					break;
				}
				case T.POLYGON:
				{
					result = coords;
					break;
				}
			}
			return result;
		}
	}
};

/**
 * Help methods to manipulate renderOptions property of a chem structure object.
 * @class
 */
Kekule.Render.RenderOptionUtils = {
	/**
	 * Create a new options object, inherits settings from options and local renderOptions of obj.
	 * @param {Kekule.ChemObject} obj
	 * @param {Hash} options
	 * @returns {Hash}
	 */
	mergeObjLocalRenderOptions: function(obj, options)
	{
		/*
		var result = Object.create(options);
		if (obj && obj.getRenderOptions)
		{
			var ops = obj.getRenderOptions() || {};
			result = Object.extend(result, ops);
		}
		*/
		var childOps = (obj && obj.getOverriddenRenderOptions)? obj.getOverriddenRenderOptions(): null;

		return Kekule.Render.RenderOptionUtils.mergeRenderOptions(childOps || {}, options);
	},
	/**
	 * Create a new options object, inherits settings from options and local renderOptions of obj.
	 * @param {Kekule.ChemObject} obj
	 * @param {Hash} options
	 * @returns {Hash}
	 */
	mergeObjLocalRender3DOptions: function(obj, options)
	{
		/*
		 var result = Object.create(options);
		 if (obj && obj.getRenderOptions)
		 {
		 var ops = obj.getRenderOptions() || {};
		 result = Object.extend(result, ops);
		 }
		 */
		var childOps = (obj && obj.getOverriddenRender3DOptions)? obj.getOverriddenRender3DOptions(): null;

		return Kekule.Render.RenderOptionUtils.mergeRenderOptions(childOps || {}, options);
	},
	/**
	 * Merge childOptions into parentOptions.
	 * @param {Hash} childOptions
	 * @param {Hash} parentOptions
	 * @returns {Hash}
	 */
	mergeRenderOptions: function(childOptions, parentOptions)
	{
		var result = Object.create(parentOptions);
		result = Object.extend(result, childOptions, true, true);
		return result;
	},
	getColor: function(renderOptions)
	{
		return renderOptions? renderOptions.color: null;
	},
	/**
	 * Get molecule display type from render options.
	 * @param {Object} renderOptions
	 * @returns {Int} Value from {@link Kekule.Render.MoleculeDisplayType}.
	 */
	getMoleculeDisplayType: function(renderOptions)
	{
		return renderOptions? renderOptions.moleculeDisplayType: null;
	},
	/**
	 * Get display mode of a node.
	 * @param {Object} renderOptions
	 * @returns {Int} Value from {@link Kekule.Render.NodeLabelDisplayMode}.
	 */
	getNodeDisplayMode: function(renderOptions)
	{
		return renderOptions? renderOptions.nodeDisplayMode: null;
	},
	/**
	 * Get hydrongen display level of a node.
	 * @param {Object} renderOptions
	 * @returns {Int}
	 */
	getHydrogenDisplayLevel: function(renderOptions)
	{
		return renderOptions? renderOptions.hydrogenDisplayLevel: null;
	},
	/**
	 * Check whether charge should be displayed.
	 * @param {Object} renderOptions
	 * @returns {Bool}
	 */
	getShowCharge: function(renderOptions)
	{
		return renderOptions? renderOptions.showCharge: null;
	},
	getChargeDrawOptions: function(renderOptions)
	{
		if (!renderOptions)
			return null;
		else
		{
			var props = ['showCharge', 'chargeMarkType', 'chargeMarkFontSize',
				'chargeMarkMargin',
				'chargeMarkCircleWidth',  // width of circle stroke
				'color', 'opacity'];
			var result = Object.copyValues({}, renderOptions, props);
			return result;
		}
	},
	/**
	 * Retrieve useful options to draw rich text (including font size, font family and so on) from renderOptions.
	 * @param {Object} renderOptions
	 * @returns {Hash}
	 */
	getNodeLabelDrawOptions: function(renderOptions)
	{
		if (!renderOptions)
			return null;
		else
		{
			var props = ['fontSize', 'fontFamily', 'supFontSizeRatio', 'subFontSizeRatio',
				'superscriptOverhang', 'subscriptOversink', 'textBoxXAlignment', 'textBoxYAlignment',
				'color', 'opacity'];
			var result = Object.copyValues({}, renderOptions, props);
			return result;
		}
	},
	/**
	 * Retrieve bond render type from renderOptions of a chem object.
	 * @param {Object} renderOptions
	 * @returns {Int}
	 */
	getConnectorRenderType: function(renderOptions)
	{
		return renderOptions? renderOption.renderType: null;
	},
	/**
	 * Retrieve params for drawing connectors.
	 * @param {Object} renderOptions
	 * @returns {Hash}
	 */
	getConnectorDrawParams: function(renderOptions)
	{
		if (!renderOptions)
			return null;
		else
		{
			var props = ['bondLineWidth', 'boldBondLineWidth', 'hashSpacing', 'multipleBondSpacingRatio',
				'multipleBondSpacingAbs', 'multipleBondMaxAbsSpacing', 'bondArrowLength', 'bondArrowWidth',
				'bondWedgeWidth', 'bondWedgeHashMinWidth', 'color', 'opacity'];
			var result = Object.copyValues({}, renderOptions, props);
			return result;
		}
	},

	/*
	 * Get rich text draw options from textFontConfigs and drawOptions.
	 * Settings in drawOptions may override the ones in textFontConfigs.
	 * This function is used by ctab or formula renderer.
	 * @param {Kekule.Render.Render2DConfigs} render2DConfigs
	 * @param {Hash} drawOptions
	 * @returns {Hash}
	 * @deprecated
	 */
	/*
	extractRichTextDraw2DOptions: function(render2DConfigs, drawOptions)
	{
		var oneOf = Kekule.oneOf;
		var textFontConfigs = render2DConfigs.getTextFontConfigs();
		var op = {
					'fontSize': oneOf(drawOptions.fontSize, render2DConfigs.getLengthConfigs().getAtomFontSize()),
					'fontFamily': oneOf(drawOptions.fontFamily, textFontConfigs.getAtomFontFamily()),
					'supFontSizeRatio': oneOf(drawOptions.supFontSizeRatio, textFontConfigs.getSupFontSizeRatio()),
					'subFontSizeRatio': oneOf(drawOptions.subFontSizeRatio, textFontConfigs.getSubFontSizeRatio()),
					'superscriptOverhang': oneOf(drawOptions.superscriptOverhang, textFontConfigs.getSuperscriptOverhang()),
					'subscriptOversink': oneOf(drawOptions.subscriptOversink, textFontConfigs.getSubscriptOversink()),
					/ *
					'textBoxXAlignment': Kekule.Render.BoxXAlignment.CENTER,
					'textBoxYAlignment': Kekule.Render.BoxYAlignment.CENTER,
					* /
					'color': oneOf(drawOptions.atomColor, drawOptions.color, render2DConfigs.getColorConfigs().getAtomColor()),
					'opacity': oneOf(drawOptions.opacity, render2DConfigs.getGeneralConfigs().getDrawOpacity())
				};
		return op;
	},
	*/

	convertConfigsToPlainHash: function(configs)
	{
		var U = Kekule.Render.RenderOptionUtils;
		if (configs instanceof Kekule.Render.Render3DConfigs)
			return U.convert3DConfigsToPlainHash(configs)
		else
			return U.convert2DConfigsToPlainHash(configs);
	},

	/**
	 * Convert the whole {@link Kekule.Render.Render2DConfigs} instance into a
	 * one-level hash object.
	 * @param {Kekule.Render.Render2DConfigs} render2DConfigs
	 * @returns {Hash}
	 */
	convert2DConfigsToPlainHash: function(render2DConfigs)
	{
		var OU = Kekule.ObjUtils;
		var result = {};
		// keep a ref to config instance
		result._configs = render2DConfigs;

		// general configs
		var h = render2DConfigs.getGeneralConfigs().toHash();
		OU.replacePropName(h, 'drawOpacity', 'opacity');
		result = Object.extend(result, h);

		// moleculeDisplayConfigs
		h = render2DConfigs.getMoleculeDisplayConfigs().toHash();
		OU.replacePropName(h, 'defMoleculeDisplayType', 'moleculeDisplayType');
		OU.replacePropName(h, 'defNodeDisplayMode', 'nodeDisplayMode');
		OU.replacePropName(h, 'defHydrogenDisplayLevel', 'hydrogenDisplayLevel');
		OU.replacePropName(h, 'defChargeMarkType', 'chargeMarkType');
		result = Object.extend(result, h);

		// displayLabelConfigs, special, keep the whole config object
		result.displayLabelConfigs = render2DConfigs.getDisplayLabelConfigs();

		// textFontConfigs
		h = render2DConfigs.getTextFontConfigs().toHash();
		result = Object.extend(result, h);

		// lengthConfigs
		h = render2DConfigs.getLengthConfigs().toHash();
		result = Object.extend(result, h);
		result.unitLength = result.unitLength || 1;

		// colorConfigs
		h = render2DConfigs.getColorConfigs().toHash();
		result = Object.extend(result, h);

		return result;
	},
	/**
	 * Convert the whole {@link Kekule.Render.Render3DConfigs} instance into a
	 * one-level hash object.
	 * @param {Kekule.Render.Render3DConfigs} render3DConfigs
	 * @returns {Hash}
	 */
	convert3DConfigsToPlainHash: function(render3DConfigs)
	{
		var OU = Kekule.ObjUtils;
		var result = {};
		// keep a ref to config instance
		result._configs = render3DConfigs;

		// generalConfigs
		var h = render3DConfigs.getGeneralConfigs().toHash();
		OU.replacePropName(h, 'drawOpacity', 'opacity');
		result = Object.extend(result, h);

		// environmentConfigs
		var h = render3DConfigs.getEnvironmentConfigs().toHash();
		result = Object.extend(result, h);

		// moleculeDisplayConfigs
		var h = render3DConfigs.getMoleculeDisplayConfigs().toHash();
		OU.replacePropName(h, 'defMoleculeDisplayType', 'moleculeDisplayType');
		OU.replacePropName(h, 'defBondSpliceMode', 'bondSpliceMode');
		OU.replacePropName(h, 'defDisplayMultipleBond', 'displayMultipleBond');
		OU.replacePropName(h, 'defBondColor', 'bondColor');
		OU.replacePropName(h, 'defAtomColor', 'atomColor');
		result = Object.extend(result, h);

		// modelConfigs
		var h = render3DConfigs.getModelConfigs().toHash();
		result = Object.extend(result, h);

		// lengthConfigs
		var h = render3DConfigs.getLengthConfigs().toHash();
		result = Object.extend(result, h);

		return result;
	},

	/**
	 * Returns common render option or render 3D option value of multiple chemObjs.
	 * If values in objects are not same, null will be returned.
	 * @param {Array} chemObjs
	 * @param {String} propName
	 * @param {Bool} is3DOption
	 * @returns {Variant}
	 * @private
	 */
	getCascadeRenderOptionValueOfObjs: function(chemObjs, propName, is3DOption)
	{
		var result;
		var resultSet = false;
		var objs = Kekule.ArrayUtils.toArray(chemObjs);
		var getFunc = is3DOption? 'getCascadedRender3DOption': 'getCascadedRenderOption';
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (obj[getFunc])
			{
				var value = obj[getFunc](propName);
				if (!resultSet)
				{
					result = value;
					resultSet = true;
				}
				else
				{
					if (result != value)  // not match
						return null;
				}
			}
		}
		return result;
	},

	/**
	 * Set render option or render 3D option value(s) to multiple chemObjs.
	 * @param {Array} chemObjs
	 * @param {Hash} options Hash of options. {renderOptionName: value}.
	 * @param {Bool} is3DOption If true, styles will be put to Render3DOptions, otherwise RenderOptions will be set.
	 * @private
	 */
	setRenderOptionValueOfObjs: function(chemObjs, options, is3DOption)
	{
		var objs = Kekule.ArrayUtils.toArray(chemObjs);
		var setFunc = is3DOption? 'setRenderOption': 'setRenderOption';
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (obj[setFunc])
			{
				var propNames = Kekule.ObjUtils.getOwnedFieldNames(options)
				for (var j = 0, k = propNames.length; j < k; ++j)
				{
					obj[setFunc](propNames[j], options[propNames[j]]);
				}
			}
		}
	}
};

/**
 * Help methods to manipulate render3DOptions property of a chem structure object.
 * @class
 */
Kekule.Render.Render3DOptionUtils = {
	/**
	 * Retrieve bond render type from render3DOptions of a chem object.
	 * @param {Object} renderOptions
	 * @returns {Int}
	 */
	getConnectorRenderType: function(renderOptions)
	{
		return renderOptions? renderOptions.renderType: null;
	},

	/**
	 * Convert the whole {@link Kekule.Render.Render3DConfigs} instance into a
	 * one-level hash object.
	 * @param {Kekule.Render.Render2DConfigs} render3DConfigs
	 * @returns {Hash}
	 */
	convertConfigsToPlainHash: function(render3DConfigs)
	{

	}
};

/**
 * Help methods to get colors used for atom.
 * @class
 */
Kekule.Render.RenderColorUtils = {
	/**
	 * Return active color set used for atoms.
	 * @param {Int} rendererType Indicate the color will be used in 2D or 3D renderer.
	 * @return {Array}
	 */
	getActiveAtomColorSet: function(rendererType)
	{
		return Kekule.Render.atomColors[rendererType];
	},
	/**
	 * Set active color set for atoms.
	 * @param {String} newSetName
	 * @param {Int} rendererType Indicate the color will be used in 2D or 3D renderer.
	 */
	setActiveAtomColorSet: function(newSetName, rendererType)
	{
		var r = Kekule.Render.AtomColorSets[newSetName];
		if (r)
			Kekule.Render.atomColorSet[rendererType] = r;
		return r;
	},
	/**
	 * Get color for an atom.
	 * @param {Object} atomicNumber 0 is for default color for non-atom node.
	 * @param {Int} rendererType Indicate the color will be used in 2D or 3D renderer.
	 * @returns {String} '#RRGGBB' format color.
	 */
	getColor: function(atomicNumber, rendererType)
	{
		var s = Kekule.Render.RenderColorUtils.getActiveAtomColorSet(rendererType);
		var r = s[atomicNumber];
		if (!r)
			r = s[0];  // default one
		return r;
	}
};

/** @ignore */
Kekule.Render.UpdateObjUtils = {
	/** @ignore */
	_extractObjsOfUpdateObjDetails: function(updatedObjDetails)
	{
		var result = [];
		for (var i = 0, l = updatedObjDetails.length; i < l; ++i)
		{
			var detail = updatedObjDetails[i];
			if (detail.obj)
				Kekule.ArrayUtils.pushUnique(result, detail.obj);
		}
		return result;
	},
	/** @ignore */
	_createUpdateObjDetailsFromObjs: function(updatedObjs)
	{
		var result = [];
		for (var i = 0, l = updatedObjs.length; i < l; ++i)
		{
			result.push({'obj': updatedObjs[i]});
		}
		return result;
	}
};


/**
 * Helper class to define some common methods of renderer.
 * Note: as the introducing of the base class {@link Kekule.Render.CompositeRenderer},
 * almost all renderers support child renderers now, so this helper class should not be used again.
 * @deprecated
 * @ignore
 * @class
 */
Kekule.Render.RendererDefineUtils = {
	/// Methods to expand support for compositeObjRenderer
	/**
	 * @class
	 * @private
	 */
	CompositeObjRendererMethods:
	/** @lends Kekule.Render.RendererDefineUtil.CompositeObjRendererMethods# */
	{
		/*
		initialize: function($super, chemObj, drawBridge, parent)
		{
			$super(chemObj, drawBridge, parent);
		},
		*/
		/** @ignore */
		finalize: function($super)
		{
			this.reset();
			$super();
		},
		/** @private */
		doEstimateObjBox: function(context, options, allowCoordBorrow)
		{
			var result = null;
			var renderers = this.prepareChildRenderers();
			var BU = Kekule.BoxUtils;
			for (var i = 0, l = renderers.length; i < l; ++i)
			{
				var r = renderers[i];
				if (r)
				{
					var b = r.estimateObjBox(context, options, allowCoordBorrow);
					if (b)
					{
						if (!result)
							result = BU.clone(b); //Object.extend({}, b);
						else
							result = BU.getContainerBox(result, b);
					}
				}
			}
			return result;
		},
		/** @ignore */
		isChemObjRenderedBySelf: function($super, context, obj)
		{
			var result = $super(context, obj);
			if (!result)
			{
				var childRenderers = this.getChildRenderers();
				for (var i = 0, l = childRenderers.length; i < l; ++i)
				{
					var r = childRenderers[i];
					if (r.isChemObjRenderedBySelf(context, obj))
						return true;
				}
			}
			if (!result)
			{
				this.refreshChildObjs();
				var objs = this.getTargetChildObjs();
				result = (objs && objs.indexOf(obj) >= 0);
				//console.log('here', this.getClassName(), obj.getClassName(), result);
			}
			return result;
		},
		/** @ignore */
		isChemObjRenderedDirectlyBySelf: function($super, context, obj)
		{
			return $super(context, obj);
		},
		/**
		 * Returns all children of this.getChemObj(). Descendants must override this method.
		 * If no children is found, null should be returned.
		 * @returns {Array}
		 * @private
		 */
		getChildObjs: function()
		{
			return null;
		},
		/**
		 * Prepare all child objects to be drawn.
		 * @private
		 */
		prepareChildObjs: function()
		{
			var childObjs = this.getTargetChildObjs();
			if (childObjs)  // already prepared
				return childObjs;

			this.setTargetChildObjs(this.getChildObjs());
			return this.getTargetChildObjs();
		},
		/** @private */
		refreshChildObjs: function()
		{
			this.setTargetChildObjs(null);
			this.prepareChildObjs();
		},
		/** @private */
		getChildRenderers: function()
		{
			return this.getChildRendererMap().getValues();
		},
		/** @private */
		getRendererForChild: function(childObj, canCreate)
		{
			var renderSelector = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
				Kekule.Render.get3DRendererClass: Kekule.Render.get2DRendererClass;
			var rendererMap = this.getChildRendererMap();
			var result = rendererMap.get(childObj);
			if (!result && canCreate)
			{
				var c = renderSelector(childObj) || Kekule.Render.DummyRenderer;  // dummy renderer, do nothing
				var result = c? new c(childObj, this.getDrawBridge(), /*this.getRenderConfigs(),*/ this): null;  // renderer may be null for some unregistered objects
				rendererMap.set(childObj, result);
				result.setRedirectContext(this.getRedirectContext());
			}
			return result;
		},
		/**
		 * Prepare renders to draw child objects.
		 * @private
		 */
		prepareChildRenderers: function()
		{
			/*
			var childRenderers = this.getChildRenderers();
			if (childRenderers)
				return childRenderers;
			childRenderers = [];
			this.setChildRenderers(childRenderers);
			*/
			var rendererMap = this.getChildRendererMap();

			var childObjs = this.prepareChildObjs() || [];

			// remove unneed renderers
			var oldRenderedObjs = rendererMap.getKeys();
			for (var i = 0, l = oldRenderedObjs.length; i < l; ++i)
			{
				var obj = oldRenderedObjs[i];
				if (childObjs.indexOf(obj) < 0)
					rendererMap.remove(obj);
			}
			// add new renderers if needed
			for (var i = 0, l = childObjs.length; i < l; ++i)
			{
				var childObj = childObjs[i];
				/*
				if (!rendererMap.get(childObj))
				{
					var c = renderSelector(childObj);
					var r = c? new c(childObj, this.getDrawBridge(), this): null;  // renderer may be null for some unregistered objects
					//console.log('prepare new child renderer', r.getClassName(), childObj.getClassName(), childObj.getNodeCount? childObj.getNodeCount(): -1);
					//childRenderers.push(r);
					rendererMap.set(childObj, r);
				}
				*/
				this.getRendererForChild(childObj, true);
			}

			//return childRenderers;
			return rendererMap.getValues();
		},
		/**
		 * Release all child renderer instance.
		 * @private
		 */
		releaseChildRenderers: function()
		{
			var rendererMap = this.getChildRendererMap();
			//var childRenderers = this.getChildRenderers();
			var childRenderers = rendererMap.getValues();
			if (!childRenderers)
				return;
			for (var i = 0, l = childRenderers.length; i < l; ++i)
			{
				childRenderers[i].finalize();
			}
			//this.setChildRenderers(null);
			rendererMap.clear();
		},

		/** @private */
		hasChild: function()
		{
			return !!(this.getTargetChildObjs() || []).length;
		},

		/**
		 * Prepare child objects and renderers, a must have step before draw.
		 * @private
		 */
		prepare: function()
		{
			this.prepareChildObjs();
			this.prepareChildRenderers();
		},

		/**
		 * Set renderer to initialized state, clear childObjs and childRenderers.
		 * @private
		 */
		reset: function()
		{
			this.setTargetChildObjs(null);
			this.releaseChildRenderers();
		},

		/** @private */
		doDraw: function($super, context, baseCoord, options)
		{
			// prepare, calc transform options and so on
			//this.reset();
			this.prepare();
			if (!this.hasChild())
				return $super(context, baseCoord, options);
			else  // then draw each child objects by child renderers
				return this.doDrawChildrenAndSelf(context, baseCoord, options);
		},
		/** @private */
		doDrawChildrenAndSelf: function(context, baseCoord, options)
		{
			var group = this.createDrawGroup(context);
			var childRenderers = this.getChildRenderers();
			/*
			 var coordMode = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
			 Kekule.CoordMode.COORD3D: Kekule.CoordMode.COORD2D;
			 var transformFunc = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
			 Kekule.CoordUtils.transform3D: Kekule.CoordUtils.transform2D;
			 */

			var ops = Object.create(options);
			/*
			var chemObj = this.getChemObj();
			var objOptions = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
				chemObj.getOverriddenRender3DOptions(): chemObj.getOverriddenRenderOptions();

			ops = Object.extend(ops, objOptions || {});
			*/

			//console.log('overrided ops', chemObj.getClassName(), ops, objOptions);

			this.getRenderCache(context).childDrawOptions = ops;

			for (var i = 0, l = childRenderers.length; i < l; ++i)
			{
				var r = childRenderers[i];
				var baseCoord = null;
				/*
				 var chemObj = r.getChemObj();
				 if (chemObj.getAbsBaseCoordOfMode)
				 {
				 var baseCoord = chemObj.getAbsBaseCoordMode(coordMode);
				 baseCoord = transformFunc(baseCoord, options.transformParams);
				 }
				 //console.log('drawOptions', options);
				 */
				var elem = r.draw(context, baseCoord, ops);
				if (group && elem)
					this.addToDrawGroup(elem, group);
			}
			//console.log('drawn group', group);

			// self
			var selfElem = this.doDrawSelf(context, baseCoord, options);
			if (selfElem)
				 this.addToDrawGroup(selfElem, group);

			return group;
		},
		/**
		 * Draw only self, without child objects.
		 * Descendants may override this method.
		 * @private
		 */
		doDrawSelf: function($super, context, baseCoord, options)
		{
			return $super(context, baseCoord, options);
		},

		/* @private */
		/*
		doRedraw: function(context)
		{
			//console.log('redraw', this.getClassName());
			return this.doRedrawCore(context);
		},
		*/
		/* @private */
		/*
		doRedrawCore: function(context, options)
		{
			var group = this.createDrawGroup(context);
			var childRenderers = this.getChildRenderers();

			for (var i = 0, l = childRenderers.length; i < l; ++i)
			{
				var r = childRenderers[i];
				var elem = r.redraw(context)
				if (group && elem)
					this.addToDrawGroup(elem, group);
			}
			return group;
		},
		*/

		/** @private */
		doClear: function($super, context)
		{
			if (this.hasChild())
			{
				var childRenderers = this.getChildRendererMap().getValues();
				for (var i = 0, l = childRenderers.length; i < l; ++i)
				{
					if (childRenderers[i])
					{
						childRenderers[i].clear(context);
					}
				}
			}
			//this.doClearSelf(context);
			$super(context);
			return true;
		},
		/**
		 * Clear only self, without child objects.
		 * Descendants may override this method.
		 * @private
		 */
		doClearSelf: function($super, context)
		{
			return $super(context);
		},
		/** @private */
		doUpdate: function($super, context, updateObjDetails, updateType)
		{
			//if (!this.hasChild())
				return $super(context, updateObjDetails, updateType);

			var updatedObjs = Kekule.Render.UpdateObjUtils._extractObjsOfUpdateObjDetails(updateObjDetails);
			//console.log(this.getClassName(), updateType, updateObjDetails);

			//console.log(updatedObjs);

			//this.setTargetChildObjs(null);
			//this.prepareChildObjs();  // update childObjs

			//var directChildren = this.getChildObjs();
			var directChildren = this.getTargetChildObjs() || [];
			var childRendererMap = this.getChildRendererMap();
			var objs = Kekule.ArrayUtils.toArray(updatedObjs);
			var objsMap = new Kekule.MapEx(false);
			var renderers = [];
			var redrawRoot = false;

			for (var i = 0, l = objs.length; i < l; ++i)
			{
				var obj = objs[i];
				if (this.isChemObjRenderedDirectlyBySelf(context, obj))  // need redraw self
				{
					redrawRoot = true;
				}
			}
			if (redrawRoot)
			{
				//console.log('redraw root', this.getClassName());
				this.doClear(context);
				this.redraw(context);
				return true;
			}

			for (var i = 0, l = objs.length; i < l; ++i)
			{
				var obj = objs[i];

				/*
				// debug
				var isMol = false;
				if (obj instanceof Kekule.StructureFragment)
				{
					console.log('obj update here', obj.getId());
					isMol = true;
				}
				*/

				/*  // TODO: now has bugs, disable it currently
				if (this.isChemObjRenderedDirectlyBySelf(context, obj))  // the root object it self updated, need re-render self
				{
					//console.log('do redraw');
					redrawRoot = true;
					//return true;
				}
				*/

				var renderer = childRendererMap.get(obj);

				if (renderer)  // is direct child and has a corresponding renderer
				{
					/*
					// debug
					if (isMol)
					{
						console.log('find renderer', renderer.getClassName(), updateType, obj.getId());
						if (!renderer.id)
							renderer.id = obj.getId();
						else if (renderer.id !== obj.getId())
							console.log('id conflivt', renderer.id, obj.getId())
					}
					*/
					// check update type, if updateType is remove, just remove the renderer
					if (updateType === Kekule.Render.ObjectUpdateType.REMOVE)
					{
						renderer.clear();
						childRendererMap.remove(obj);
					}
					else
					{
						Kekule.ArrayUtils.pushUnique(renderers, renderer);
						//renderers.push(renderer);
						olds = [obj];
						objsMap.set(renderer, olds);
					}
				}
				else
				{
					var rs = this._getRenderersForChildObj(context, obj);
					if (!rs.length)
					{
						if (directChildren.indexOf(obj) >= 0)  // still can not find
						{
							var r = this.getRendererForChild(obj, true);
							/*
							if (r)
								rs.push(r);
							*/

							//console.log('here', r.getClassName());
							// draw directly
							var drawnResult = r.draw(context, null, this.getRenderCache(context).childDrawOptions);
							if (drawnResult)
							{
								var drawnElem = this.getCachedDrawnElem(context);
								if (drawnElem)
									this.addToDrawGroup(drawnResult, drawnElem);
							}
						}
					}
					for (var j = 0, k = rs.length; j < k; ++j)
					{
						var renderer = rs[j];
						var olds = objsMap.get(renderer);
						if (!olds)
						{
							renderers.push(renderer);
							olds = [];
							objsMap.set(renderer, olds);
						}
						olds.push(obj);
					}
				}
			}

			//console.log('do update', this.getClassName(), renderers);

			// apply update in each renderer
			var result = true;
			for (var i = 0, l = renderers.length; i < l; ++i)
			{
				var renderer = renderers[i];
				//console.log(renderer);
				var o = objsMap.get(renderer);
				//console.log('call update', renderer.getClassName(), o);
				var details = Kekule.Render.UpdateObjUtils._createUpdateObjDetailsFromObjs(o);
				/*
				// debug
				if (renderer.id)
					console.log('renderer id', renderer.id);
				if (renderer.getChemObj() instanceof Kekule.StructureFragment)
				{
					console.log('update renderer', renderer.getClassName(), updateType, o);
				}
				*/
				var r = renderer.update(context, details, updateType);
				result = result && r;

				/*
				if (!result)  // fail to update part, need to repaint whole
				{
					break;
				}
				*/
			}

			var result = renderers && renderers.length > 0;

			/*
			//if (redrawRoot)
			if (!result)
			{

				this.doClear(context);
				this.redraw(context);
				result = true;

			}
			*/

			//result = $super(context, updateObjDetails, updateType) && result;

			if (!result)
			{
				result = true;
			}

			//console.log('update', updatedObjs[0].getClassName(), this.getChemObj().getClassName(), updatedObjs[0] === this.getChemObj(), renderers.length);

			return result;
		},
		/** @private */
		doUpdateSelf: function($super, context, updateObjDetails, updateType)
		{
			return $super(context, updateObjDetails, updateType);;
		},
		/** @private */
		_getRenderersForChildObj: function(context, childObj)
		{
			var result = [];
			var childRenderers = this.getChildRenderers();
			for (var i = 0, l = childRenderers.length; i < l; ++i)
			{
				var r = childRenderers[i];
				if (r.isChemObjRenderedBySelf(context, childObj))
					result.push(r);
			}
			return result;
		}
	},

	/**
	 * Define composite render properties and methods to a class.
	 * @param {Object} aClass Should be class object.
	 */
	addCompositeRenderSupport: function(aClass)
	{
		ClassEx.defineProp(aClass, 'targetChildObjs', {
			'dataType': DataType.ARRAY,
			'serializable': false
		});
		/*
		ClassEx.defineProp(aClass, 'childRenderers', {
			'dataType': DataType.ARRAY,
			'serializable': false
		});
		*/
		ClassEx.defineProp(aClass, 'childRendererMap', {
			'dataType': DataType.OBJECT,
			'serializable': false,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('childRendererMap');
				if (!result)
				{
					result = new Kekule.MapEx(true);  // non-weak map, as we should store the renderers
					this.setPropStoreFieldValue('childRendererMap', result);
				}
				return result;
			}
		});
		ClassEx.extend(aClass, Kekule.Render.RendererDefineUtils.CompositeObjRendererMethods);
	}
};