/**
 * @fileoverview
 * This file contains basic classes to represent structural objects in molecule.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /data/kekule.dataUtils.js
 * requires /core/kekule.elements.js
 * requires /core/kekule.electrons.js
 * requires /core/kekule.valences.js
 * requires /utils/kekule.utils.js
 * requires /localizations/
 */

(function() {
"use strict";

var AU = Kekule.ArrayUtils;

/**
 * Enumeration of comparation of chem structure.
 * @enum
 */
Kekule.StructureComparationLevel = {
	/** Compare only topological graph, atom/bond details are ignored. */
	SKELETAL: 1,
	/** Compare only constitution, ignore stereo factors and charge. */
	CONSTITUTION: 2,
	/** Compare with stereo factors but ignore atom mass number and charge. */
	CONFIGURATION: 3,
	/** Compare with stereo factors and mass number / charge. */
	EXACT: 4,
	/** Default comparation level. */
	DEFAULT: 4
};

Kekule.globalOptions.add('structure', {
	defaultBondLength2D: 0.8,  // a default C-C bond length for generating 2D coordinates of new structures
	defaultBondLength3D: 1.5  // a default C-C bond length for generating 3D coordinates of new structures
});
/*
 * Default options to compare chem structures.
 * @object
 */
/*
Kekule.globalOptions.structureComparation = {
	structureComparationLevel: Kekule.StructureComparationLevel.DEFAULT
};
*/

Kekule.globalOptions.add('algorithm.structureComparation', {
	structureComparationLevel: Kekule.StructureComparationLevel.DEFAULT
});
Kekule.globalOptions.add('algorithm.structureClean', {
	structureCleanOptions: {
		'orphanChemNode': true,
		'hangingChemConnector': true
	}
});

// The target atom element that should be applied implicit H estimation.
// Defaultly all elements should be applied. If some need to be excluded, you need to
// explicitly set its value to false.
Kekule.globalOptions.add('structure.implicitHydrogenEstimationStrategy', {
	targetElementCategories: {

	},
	targetElements: {

	},
	targetOrphanAtomElementCategories: {

	},
	targetOrphanAtomElements: {

	}
});
Kekule.globalOptions.structure.implicitHydrogenEstimationStrategy.targetOrphanAtomElementCategories[Kekule.ElementCategory.METAL] = false;

// extend method to Kekule.ObjComparer
Kekule.ObjComparer.compareStructure = function(obj1, obj2, options)
{
	var ops = Object.create(options || {});
	ops.method = Kekule.ComparisonMethod.CHEM_STRUCTURE;
	return Kekule.ObjComparer.compare(obj1, obj2, ops);
};
Kekule.ObjComparer.equalStructure = function(obj1, obj2, options)
{
	return Kekule.ObjComparer.compareStructure(obj1, obj2, options) === 0;
};
Kekule.ObjComparer.getStructureComparisonDetailOptions = function(initialOptions)
{
	var result = Object.extend({}, initialOptions);
	if (initialOptions /* && initialOptions.method === Kekule.ComparisonMethod.CHEM_STRUCTURE */)
	{
		var CL = Kekule.StructureComparationLevel;
		var level = initialOptions.structureLevel || initialOptions.level  // options.level for backward compatible
				|| Kekule.globalOptions.algorithm.structureComparation.structureComparationLevel;
		//if (Kekule.ObjUtils.notUnset(level))
		{
			var affectedFields = [
				'atom', 'mass', 'linkedConnectorCount', 'charge', 'radical', 'stereo',
				'hydrogenCount', 'connectedObjCount', 'bondType', 'bondOrder'
			];
			var detailOps;
			if (level === CL.SKELETAL)
				detailOps = {
					atom: false, mass: false, linkedConnectorCount: true, charge: false, radical: false,
					stereo: false, hydrogenCount: false,
					connectedObjCount: true, bondType: false, bondOrder: false
				};
			else if (level === CL.CONSTITUTION)
				detailOps = {
					atom: true, mass: false, linkedConnectorCount: true, charge: false, radical: false,
					stereo: false, hydrogenCount: true,
					connectedObjCount: true, bondType: true, bondOrder: true
				};
			else if (level === CL.CONFIGURATION)
				detailOps = {
					atom: true, mass: false, linkedConnectorCount: true, charge: false, radical: false,
					stereo: true, hydrogenCount: true,
					connectedObjCount: true, bondType: true, bondOrder: true
				};
			else if (level === CL.EXACT)
				detailOps = {
					atom: true, mass: true, linkedConnectorCount: true, charge: true, radical: true,
					stereo: true, hydrogenCount: true,
					connectedObjCount: true, bondType: true, bondOrder: true
				};

			// add compareXXX field to result, for backward compatibility
			/*
			var fields = Kekule.ObjUtils.getOwnedFieldNames(detailOps);
			for (var i = 0, l = fields.length; i < l; ++i)
			{
				var fieldName = fields[i];
				var value = detailOps[fieldName];
				var compatibleName = 'compare' + fieldName.capitalizeFirst();
				detailOps[compatibleName] = value;
			}
			*/
			for (var i = 0, l = affectedFields.length; i < l; ++i)
			{
				var fieldName = affectedFields[i];
				var compatibleFieldName = 'compare' + fieldName.capitalizeFirst();  // backward compatible
				var oldValue = result[fieldName];
				if (Kekule.ObjUtils.isUnset(oldValue))
					oldValue = result[compatibleFieldName];
				if (Kekule.ObjUtils.isUnset(oldValue))
				{
					result[fieldName] = detailOps[fieldName];
					result[compatibleFieldName] = detailOps[fieldName];
				}
				else
				{
					result[fieldName] = oldValue;
					result[compatibleFieldName] = oldValue;
				}
			}

			// override bool values
			//result = Object.extend(detailOps || {}, result);
		}
	}
	return result;
};

/**
 * An abstract structure object, either a node or a connector.
 * @class
 * @augments Kekule.ChemObject
 * @property {Kekule.ChemStructureObject} parent Parent of this object.
 *   For example, molecule is parent of its atoms and bonds.
 * @property {Array} linkedConnectors The connectors connected with this object.
 * @property {Array} linkedObjs Objects connected with this one through linkedConnectors. Read only.
 * @property {Array} linkedSiblings Sibling objects connected with this one through linkedConnectors. Read only.
 *   Note: if there are sub structures (subgroups) in connection table, and a object is linked with a inside object inside subgroup,
 *   linkedSiblings will returns the subgroup rather than the inside object.
 * @property {Hash} structureCache Cached complex structure data, e.g. ring info.
 *   The cache will be automatically cleared when the structure is changed unless property (@link Kekule.ChemStructureObject.autoClearStructureCache} is false.
 * @property {Bool} autoClearStructureCache Default is true, automatically clear structure cache data when structure object is changed.
 *   Note: this property is not serializable and should be set manually.
 */
/**
 * Invoked when object is changed and the change is related with structure
 * (e.g. modify a bond, change a atomic number...).
 * Event has field: {origin: the change source object (may be a child of event.target}.
 * @name Kekule.ChemStructureObject#structureChange
 * @event
 */
Kekule.ChemStructureObject = Class.create(Kekule.ChemObject,
/** @lends Kekule.ChemStructureObject# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructureObject',
	/** @constructs */
	initialize: function(/*$super, */id)
	{
		this.setPropStoreFieldValue('autoClearStructureCache', true);
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
	},
	/** @ignore */
	doFinalize: function(/*$super*/)
	{
		this.tryApplySuper('doFinalize')  /* $super() */;
	},
	/** @private */
	initProperties: function()
	{
		//this.defineProp('parent', {'dataType': 'Kekule.ChemStructureObject', 'serializable': false});
		this.defineProp('linkedConnectors', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null
			/*
			'getter': function()
				{
					if (!this.getPropStoreFieldValue('linkedConnectors'))
						this.setPropStoreFieldValue('linkedConnectors', []);
					return this.getPropStoreFieldValue('linkedConnectors');
				}
			*/
		});
		this.defineProp('linkedObjs', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null,
			'getter': function()
				{
					var result = [];
					for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
					{
						var connector = this.getLinkedConnectorAt(i);
						var objs = connector.getConnectedObjs();
						for (var j = 0, k = objs.length; j < k; ++j)
						{
							var currObj = objs[j];
							if (currObj !== this && !(this.hasChildObj && this.hasChildObj(currObj)))
								Kekule.ArrayUtils.pushUnique(result, objs[j]);
						}
					}
					return result;
				}
		});
		this.defineProp('linkedSiblings', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null,
			'getter': function()
				{
					var result = [];
					var parent = this.getParent();
					for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
					{
						var connector = this.getLinkedConnectorAt(i);
						var objs = connector.getConnectedObjs();
						for (var j = 0, k = objs.length; j < k; ++j)
						{
							var obj = objs[j];
							if (obj !== this)
							{
								if (result.indexOf(obj) < 0)
								{
									if (obj.getParent() !== parent)
										obj = parent.getDirectChildOfNestedNode(obj);
									if (obj)
										result.push(obj);
								}
							}
						}
					}
					return result;
				}
		});
		this.defineProp('structureCache', {'dataType': DataType.HASH, 'serializable': false,
			// 'scope': Class.PropertyScope.PUBLIC,
			'setter': null,   // do not allow change it directly, use method setStructureCacheData instead
			'getter': function(autoCreate)
			{
				var result = this.getPropStoreFieldValue('structureCache');
				if (!result && autoCreate)
				{
					result = {};
					this.setPropStoreFieldValue('structureCache', result);
				}
				return result;
			}
		});
		this.defineProp('autoClearStructureCache', {'dataType': DataType.BOOL, 'serializable': false});
	},
	/** @private */
	initPropValues: function(/*$super*/)
	{
		this.tryApplySuper('initPropValues')  /* $super() */;
		this.setPropStoreFieldValue('linkedConnectors', []);
		this.setSuppressChildChangeEventInUpdating(true);
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'o';
	},

	/**
	 * Returns whether another object can stick to this object.
	 * Descendants may override this method.
	 * @param {Kekule.ChemStructureObject} fromObj
	 * @returns {Bool}
	 * @private
	 */
	getAcceptCoordStickFrom: function(fromObj)
	{
		return false;
	},

	/** @ignore */
	doGetActualCompareOptions: function(/*$super, */options)
	{
		if (options && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			var result = Kekule.ObjComparer.getStructureComparisonDetailOptions(options);
			if (options.customMethod)
				result.customMethod = options.customMethod;
			if (options.extraComparisonProperties)
				result.extraComparisonProperties = options.extraComparisonProperties;
			return result;
		}
		else
			return this.tryApplySuper('doGetActualCompareOptions', [options])  /* $super(options) */;
	},
	/** @private */
	_getComparisonOptionFlagValue: function(options, flagName)
	{
		var compatibleName = 'compare' + flagName.capitalizeFirst();
		var result = options[compatibleName];
		if (Kekule.ObjUtils.isUnset(result))
			result = options[flagName];
		return result;
	},
	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			return [];
		}
		else
			return this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
	},
	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)  // can not find different in $super
		{
			if (this._getComparisonOptionFlagValue(options, 'linkedConnectorCount'))
			{
				var c1 = this.getLinkedNonHydrogenConnectors();
				var c2 = targetObj.getLinkedNonHydrogenConnectors && targetObj.getLinkedNonHydrogenConnectors();
				result = this.doCompareOnValue(c1.length, c2 && c2.length, options);
			}
		}
		return result;
	},
	/**
	 * Explicit set compare method to chem structure and compare to targetObj.
	 * @param {Kekule.ChemObject} targetObj
	 * @param {Hash} options
	 * @returns {Int}
	 */
	compareStructure: function(targetObj, options)
	{
		var ops = Object.create(options || {});
		ops.method = Kekule.ComparisonMethod.CHEM_STRUCTURE;
		return this.compare(targetObj, ops);
	},
	/**
	 * Check if this object and targetObj has equivalent chem structure.
	 * @param {Kekule.ChemObject} targetObj
	 * @param {Hash} options
	 * @returns {Bool}
	 */
	equalStructure: function(targetObj, options)
	{
		return this.compareStructure(targetObj, options) === 0;
	},

	/**
	 * If {@link Kekule.ChemStructureObject#parent} is a {@link Kekule.StructureFragment}, returns this fragment.
	 * @returns {Kekule.StructureFragment}
	 */
	getParentFragment: function()
	{
		var p = this.getParent();
		return (p instanceof Kekule.StructureFragment)? p: null;
	},
	/**
	 * Returns the root parent of {@link Kekule.StructureFragment}, rather than subgroups.
	 * @returns {Kekule.StructureFragment}
	 */
	getRootFragment: function()
	{
		var p = this.getParentFragment();
		if (p)
			return p.getRootFragment() || p;
		else
			return p;
	},

	/**
	 * Retrieve a cached structure data item.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getStructureCacheData: function(key)
	{
		var cache = this.getStructureCache();
		return cache && cache[key];
	},
	/**
	 * Set a cached structure data item.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setStructureCacheData: function(key, value)
	{
		this.getStructureCache(true)[key] = value;
		return this;
	},
	/**
	 * Clear all cached structure data.
	 */
	clearStructureCache: function()
	{
		//console.log('clear cache', this.getClassName());
		this.setPropStoreFieldValue('structureCache', null);
		return this;
	},
	/**
	 * Check if structure cache of current object should be cleared when the structure is changed.
	 * It will returns false if autoClearStructureCache property of this object (or its parent) is false.
	 */
	needAutoClearStructureCache: function()
	{
		var p = this.getParent();
		return this.getAutoClearStructureCache() && (!p || !p.needAutoClearStructureCache || p.needAutoClearStructureCache());
	},

	/** @private */
	notifyLinkedConnectorsChanged: function()
	{
		this.notifyPropSet('linkedConnectors', this.getPropStoreFieldValue('linkedConnectors'));
	},

	/**
	 * Returns self or child object that can directly linked to a connector.
	 * For atom or other simple chem object, this function should just returns self,
	 * for structure fragment, this function need to returns an anchor node.
	 * @returns {Kekule.ChemStructureObject}
	 */
	getCurrConnectableObj: function()
	{
		return this;
	},
	/**
	 * Returns self or a child object that directly linked to the connector.
	 * For atom or other simple chem object, this function should just returns self,
	 * for structure fragment, this function need to returns the anchor node linking to connector.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @returns {Kekule.ChemStructureObject}
	 */
	getActualConnectedObjToConnector: function(connector)
	{
		return this;
	},

	/**
	 * Return count of linkedConnectors.
	 * @returns {Int}
	 */
	getLinkedConnectorCount: function()
	{
		return this.getLinkedConnectors().length;
	},
	/**
	 * Get linked connector object at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemStructureConnector}
	 */
	getLinkedConnectorAt: function(index)
	{
		return this.getLinkedConnectors()[index];
	},
	/**
	 * Returns index of connector connected to node.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Int}
	 */
	indexOfLinkedConnector: function(connector)
	{
		return this.getLinkedConnectors().indexOf(connector);
	},
	/**
	 * Link a connector to this node.
	 * @param {Kekule.ChemStructureConnector} connector
	 */
	appendLinkedConnector: function(connector)
	{
		var result = this._doAppendLinkedConnector(connector);
		if (connector)
			connector._doAppendConnectedObj(this);
		return result;
	},
	/** @private */
	_doAppendLinkedConnector: function(connector)
	{
		if (!connector)
			return -1;
		var linkedConnectors = this.getPropStoreFieldValue('linkedConnectors'); // IMPORTANT: do not use getLinkedConnectors() as it may be override by descendants
		var r = Kekule.ArrayUtils.pushUniqueEx(linkedConnectors, connector);
		if (r.isPushed)
			this.notifyLinkedConnectorsChanged();
		//console.log('append linked connector', linkedConnectors.length, this.getLinkedConnectors().length);
		return r.index;
	},
	/**
	 * Insert an connector to linkedConnectors array at index. If index is not set, connector will be inserted as the first one in linkedConnectors.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @param {Int} index
	 * @returns {Int} Index of newly inserted connector.
	 */
	insertLinkedConnectorAt: function(connector, index)
	{
		if (!index)
			index = 0;
		var i = this.indexOfLinkedConnector(connector);
		var connectors = this.getLinkedConnectors();
		if (i >= 0)  // already inside, adjust position
		{
			connectors.splice(i, 1);
			connectors.splice(index, 0, connector);
			this.notifyLinkedConnectorsChanged();
		}
		else // new one
		{
			connectors.splice(index, 0, connector);
			if (connector)
				connector._doAppendConnectedObj(this);
			this.notifyLinkedConnectorsChanged();
		}
	},
	/**
	 * Insert an connector to linkedConnectors array before refSibling. If refSibling is not set, connector will be push to the tail of linkedConnectors.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @param {Kekule.ChemStructureConnector} refSibling
	 * @returns {Int} Index of newly inserted connector.
	 */
	insertLinkedConnectorBefore: function(connector, refSibling)
	{
		var refIndex = refSibling? this.indexOfLinkedConnector(refSibling): -1;
		if (refIndex < 0)
			return this.appendLinkedConnector(connector);
		else
			return this.insertLinkedConnectorAt(connector, refIndex);
	},
	/**
	 * Remove connector at index of linkedConnectors.
	 * @param {Int} index
	 */
	removeLinkedConnectorAt: function(index)
	{
		var connector = this.getLinkedConnectorAt(index);
		if (connector)
			connector._doRemoveConnectedObjAt(connector.indexOfConnectedObj(this));
		return this._doRemoveLinkedConnectorAt(index);
	},
	/** @private */
	_doRemoveLinkedConnectorAt: function(index)
	{
		var r = Kekule.ArrayUtils.removeAt(this.getLinkedConnectors(), index);
		if (r)
			this.notifyLinkedConnectorsChanged();
		return r;
	},
	/**
	 * Remove a connector in linkedContainer.
	 * @param {Kekule.ChemStructureConnector} connector
	 */
	removeLinkedConnector: function(connector)
	{
		var index = this.getLinkedConnectors().indexOf(connector);
		if (index >= 0)
			this.removeLinkedConnectorAt(index);
	},
	/**
	 * Get connector between this object and another object.
	 * @param {Kekule.ChemStructureObject} obj
	 * @returns {Kekule.ChemStructureConnector}
	 */
	getConnectorTo: function(obj)
	{
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var c = this.getLinkedConnectorAt(i);
			if (c.hasConnectedObj(obj))
				return c;
		}
		return null;
	},
	/**
	 * Remove this node from all linked connectors.
	 * Ths method should be called before a object is removed from a structure.
	 */
	removeThisFromLinkedConnector: function()
	{
		for (var i = this.getLinkedConnectorCount() - 1; i >= 0; --i)
		{
			var c = this.getLinkedConnectorAt(i);
			c.removeConnectedObj(this);
		}
	},

	/*
	 * Returns other objects connected to this one through all connectors.
	 * @returns {Array}
	 */
	/*
	getLinkedObjs: function()
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			var objs = connector.getConnectedObjs();
			for (var j = 0, k = objs.length; j < k; ++j)
			{
				if (objs[j] !== this)
					Kekule.ArrayUtils.pushUnique(result, objs[j]);
			}
		}
		return result;
	},
	*/
	/**
	 * Returns other objects connected to this one through connector.
	 * @returns {Array}
	 */
	getLinkedObjsOnConnector: function(connector)
	{
		var result = [];
		var objs = connector.getConnectedObjs();
		for (var j = 0, k = objs.length; j < k; ++j)
		{
			if (objs[j] !== this)
				Kekule.ArrayUtils.pushUnique(result, objs[j]);
		}
		return result;
	},
	/**
	 * Filter out connectors.
	 * @param {Function} filter A filter function with param (connector, connectorIndex), returning true to include this connector in result set.
	 * @returns {Array}
	 */
	filterConnector: function(filter)
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			if (filter(connector))
				Kekule.ArrayUtils.pushUnique(result, connector);
		}
		return result;
	},
	/**
	 * Returns connectors that connected to a non hydrogen node.
	 * @returns {Array}
	 */
	getLinkedNonHydrogenConnectors: function()
	{
		var self = this;
		return this.filterConnector(function(connector){
			var objs = self.getLinkedObjsOnConnector(connector);
			return (objs.length > 1) || !(objs[0].isHydrogenAtom && objs[0].isHydrogenAtom());
		});
		/*
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			if (!connector.isNormalConnectorToHydrogen || !connector.isNormalConnectorToHydrogen())
				Kekule.ArrayUtils.pushUnique(result, connector);
		}
		return result;
		*/
	},
	/**
	 * Returns connectors that connected to a concrete hydrogen node.
	 * @returns {Array}
	 */
	getLinkedHydrogenConnectors: function()
	{
		var self = this;
		return this.filterConnector(function(connector){
			var objs = self.getLinkedObjsOnConnector(connector);
			return (objs.length === 1) && (objs[0].isHydrogenAtom && objs[0].isHydrogenAtom());
		});
	},
	/**
	 * Returns linked objects except hydrogen atoms.
	 * @returns {Array}
	 */
	getLinkedNonHydrogenObjs: function()
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			var objs = connector.getConnectedObjs();
			for (var j = 0, k = objs.length; j < k; ++j)
			{
				if (objs[j] !== this && (!objs[j].isHydrogenAtom || !objs[j].isHydrogenAtom()))
				{
					Kekule.ArrayUtils.pushUnique(result, objs[j]);
				}
			}
		}
		return result;
	},
	/**
	 * Returns linked hydrogen atoms with explicit bonds.
	 * @returns {Array}
	 * @deprecated
	 */
	getLinkedHydrogenAtoms: function()
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			if (connector instanceof Kekule.Bond)
			{
				var objs = connector.getConnectedObjs();
				for (var j = 0, k = objs.length; j < k; ++j)
				{
					if (objs[j] !== this && (objs[j].isHydrogenAtom && objs[j].isHydrogenAtom()))
					{
						Kekule.ArrayUtils.pushUnique(result, objs[j]);
					}
				}
			}
		}
		return result;
	},
	/**
	 * Returns linked hydrogen atoms with explicit single covalence bonds.
	 * These hydrogen atoms can be omitted in 2D structures.
	 * @returns {Array}
	 */
	getLinkedHydrogenAtomsWithSingleBond: function()
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			if ((connector instanceof Kekule.Bond) && connector.isSingleBond())
			{
				var objs = connector.getConnectedObjs();
				if (objs.length === 2)
				{
					for (var j = 0, k = objs.length; j < k; ++j)
					{
						if (objs[j] !== this && (objs[j].isHydrogenAtom && objs[j].isHydrogenAtom()))
						{
							Kekule.ArrayUtils.pushUnique(result, objs[j]);
						}
					}
				}
			}
		}
		return result;
	},
	/**
	 * Returns the hydrogen atom count with explicit single covalence bonds.
	 * @param {Bool} includeCached If true, the hydrogen atoms removed in molecule standardization will also be counted.
	 * @returns {Int}
	 */
	getLinkedHydrogenAtomsWithSingleBondCount: function(includeCached)
	{
		var result = this.getLinkedHydrogenAtomsWithSingleBond().length || 0;
		if (includeCached)
		{
			var cachedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
			result += cachedBondHydrogenCount;
		}
		return result;
	},

	/**
	 * Returns all pathes (node-connector-node-connector-node...) to destObj.
	 * Usually this method should be used between two nodes.
	 * @param {Kekule.ChemStructureObject} destObj
	 * @returns {Array} Each item is a hash {connector, object}, indicating a path step, obj is usually a node.
	 *   If destObj === this, [] will be returned (a zero-distance path).
	 *   If there is no path between this object and destObj, null will be returned.
	 */
	getPathesToDest: function(destObj)
	{
		return this._doGetPathesToDest(destObj, []);
	},
	/** @private */
	_doGetPathesToDest: function(destObj, iteratedConnectors)
	{
		if (destObj === this)
			return [];

		var result = [];
		var pathes;
		var dupIteratedConnectors = AU.clone(iteratedConnectors);

		for (var i = 0, ii = this.getLinkedConnectorCount(); i < ii; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);

			if (dupIteratedConnectors.indexOf(connector) >= 0)
				continue;
			else
				dupIteratedConnectors.push(connector);

			var objs = connector.getConnectedObjs();
			for (var j = 0, jj = objs.length; j < jj; ++j)
			{
				var currObj = objs[j];

				if (currObj === destObj)
					pathes = [[]];
				else if (currObj === this)
					continue;
				else
				{
					pathes = currObj && currObj._doGetPathesToDest && currObj._doGetPathesToDest(destObj, dupIteratedConnectors);
				}
				if (pathes)
				{
					var currEdge = {'connector': connector, 'object': currObj};
					for (var k = 0, kk = pathes.length; k < kk; ++k)
					{
						pathes[k].unshift(currEdge);
					}
					result = result.concat(pathes);
				}
			}
		}

		if (!result.length)
			result = null;
		return result;
	},

	/**
	 * Returns property names that affects chem structure.
	 * Descendants should override this method.
	 * @private
	 */
	getStructureRelatedPropNames: function()
	{
		return ['linkedConnectors'];
	},
	/**
	 * Notify the structure of object has been changed.
	 * @param {Kekule.ChemStructureObject} originObj
	 * @private
	 */
	structureChange: function(originObj)
	{
		//console.log('structure change', originObj && originObj.getClassName(), this.getClassName(), this.needAutoClearStructureCache());
		if (this.needAutoClearStructureCache())
		{
			this.clearStructureFlags();
			this.clearStructureCache();
		}
		this.invokeEvent('structureChange', {'origin': originObj || this});
	},
	/**
	 * Clear all flags of structure object that should be changed when structure is changed.
	 * Descendants may override this method.
	 */
	clearStructureFlags: function()
	{
		// do nothing here
	},

	/** @ignore */
	relayEvent: function(/*$super, */eventName, event)
	{
		// if structureChange event is received from child object, means the whole structure of self is also changed
		// invoke a new structureChange on self and "eat" the original one
		if (eventName === 'structureChange')
			this.structureChange(event.origin);
		else
			this.tryApplySuper('relayEvent', [eventName, event])  /* $super(eventName, event) */;
	},

	/** @ignore */
	doObjectChange: function(/*$super, */modifiedPropNames)
	{
		if (Kekule.ArrayUtils.intersect(modifiedPropNames || [], this.getStructureRelatedPropNames()).length)
		{
			//console.log('change struct by',  Kekule.ArrayUtils.intersect(modifiedPropNames || [], this.getStructureRelatedPropNames()));
			this.structureChange();
		}
	}
});

/**
 * A base class of structure node, user should not create instance of this class directly.
 * @class
 * @augments Kekule.ChemStructureObject
 * @param {String} id Id of this node.
 * @param {Hash} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Hash} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Hash} coord2D The 2D coordinates of node, {x, y}.
 * @property {Hash} coord3D The 3D coordinates of node, {x, y, z}.
 * @property {Hash} absCoord2D The absolute 2D coordinates of node, {x, y}.
 * @property {Hash} absCoord3D The absolute 3D coordinates of node, {x, y, z}.
 * @property {Int} zIndex2D A special property like zIndex in HTML, indicating the position of z-stack for 2D sketch.
 *
 * @borrows Kekule.ClassDefineUtils.CommonCoordMethods#getCoordOfMode as #getCoordOfMode
 * @borrows Kekule.ClassDefineUtils.CommonCoordMethods#setCoordOfMode as #setCoordOfMode
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#fetchCoord2D as #fetchCoord2D
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#hasCoord2D as #hasCoord2D
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#get2DX as #get2DX
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#set2DX as #set2DX
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#get2DY as #get2DY
 * @borrows Kekule.ClassDefineUtils.Coord2DMethods#set2DY as #set2DY
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#fetchCoord3D as #fetchCoord3D
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#hasCoord2D as #hasCoord3D
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#get3DX as #get3DX
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#set3DX as #set3DX
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#get3DY as #get3DY
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#set3DY as #set3DY
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#get3DZ as #get3DZ
 * @borrows Kekule.ClassDefineUtils.Coord3DMethods#set3DZ as #set3DZ
 */
Kekule.SimpleStructureNode = Class.create(Kekule.ChemStructureObject,
/** @lends Kekule.SimpleStructureNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.SimpleStructureNode',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
		if (coord2D)
			this.setCoord2D(coord2D);
		if (coord3D)
			this.setCoord3D(coord3D);
	},
	initProperties: function()
	{
		this.defineProp('zIndex2D', {'dataType': DataType.INT, 'scope': Class.PropertyScope.PUBLISHED});
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['coord2D', 'coord3D']);
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'n';
	},
	/**
	 * Calculate the box to contain the object.
	 * Descendants may override this method.
	 * @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox: function(coordMode, allowCoordBorrow)
	{
		var coord = this.getAbsCoordOfMode(coordMode, allowCoordBorrow);
		return Kekule.BoxUtils.createBox(coord, coord);
	},
	/**
	 * Calculate the 2D box to contain the object.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox2D: function(allowCoordBorrow)
	{
		return this.getContainerBox(Kekule.CoordMode.COORD2D, allowCoordBorrow);
	},
	/**
	 * Calculate the 3D box to contain the object.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox3D: function(allowCoordBorrow)
	{
		return this.getContainerBox(Kekule.CoordMode.COORD3D, allowCoordBorrow);
	}
});
Kekule.ClassDefineUtils.addStandardCoordSupport(Kekule.SimpleStructureNode);

/**
 * Represent an abstract structure node (atom, atom group, or even node in path glyphs etc.).
 * @class
 * @augments Kekule.SimpleStructureNode
 *
 * @property {Kekule.ChemStructureObject} coordStickTarget If this property is set, the abs coords (2D/3D) of this node
 *   will always be the same to the target object.
 */
Kekule.BaseStructureNode = Class.create(Kekule.SimpleStructureNode,
/** @lends Kekule.BaseStructureNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.BaseStructureNode',
	/** @private */
	initProperties: function()
	{
		this.defineProp('coordStickTarget', {
			'dataType': 'Kekule.ChemStructureObject', // 'scope': Class.PropertyScope.PUBLIC,
			'objRef': true, 'autoUpdate': true,
			'getter': function()
			{
				/*
				if (this.getAllowCoordStickTo())
					return this.getPropStoreFieldValue('coordStickTarget');
				else
					return null;
				*/
				return this.getPropStoreFieldValue('coordStickTarget');
			},
			'setter': function(value)
			{
				if (value)
				{
					//console.log('has stick target', this.getClassName(), this.getParent(), value && value.getClassName());
					if (!this.getAllowCoordStickTo(value))
					{
						//console.log('not allowed', this.getClassName(), this.getParent(), value && value.getClassName());
						Kekule.chemError(Kekule.$L('ErrorMsg.COORD_STICK_NOT_ALLOWED_ON_CLASS'));
						return;
					}
					var selfOwner = this.getOwner();
					var targetOwner = value.getOwner && value.getOwner();
					if (targetOwner && selfOwner !== targetOwner)
					{
						Kekule.chemError(Kekule.$L('ErrorMsg.UNABLE_TO_STICK_TO_OTHER_OWNER_OBJ'));
						return;
					}
					if (!value.getAcceptCoordStickFrom && !value.getAcceptCoordStickFrom(this))
					{
						Kekule.chemError(Kekule.$L('ErrorMsg.INVALID_STICK_TARGET_OBJ'));
						return;
					}
					if (!value.getAbsCoordOfMode)
					{
						Kekule.chemError(Kekule.$L('ErrorMsg.UNABLE_TO_STICK_TO_OBJ_WITHOUT_ABS_COORD'));
						return;
					}
					if (value.getCoordStickTarget && value.getCoordStickTarget() === this)
					{
						Kekule.chemError(Kekule.$L('ErrorMsg.STICK_RECURSION_NOT_ALLOWED'));
						return;
					}
				}
				var old = this.getCoordStickTarget();
				this.setPropStoreFieldValue('coordStickTarget', value);
				this._coordStickTargetChanged(old, value);
			}
		});
	},

	/**
	 * Returns whether this type of node is allowed to stick to another chem object.
	 * Descendants may override this method.
	 * @param {Kekule.ChemStructureObject} dest
	 * @returns {Bool}
	 */
	getAllowCoordStickTo: function(dest)
	{
		return false;  // default do not allow stick
	},

	/**
	 * Notify tje coord stick target has been changed.
	 * Descendants may override this method.
	 * @param {Kekule.ChemObject} oldTarget
	 * @param {Kekule.ChemObject} newTarget
	 * @private
	 */
	notifyCoordStickTargetChanged: function(oldTarget, newTarget)
	{
		// do nothing here
	},

	/** @private */
	_getParentAbsCoord: function(coordMode, allowCoordBorrow)
	{
		var	parent = this.getCoordParent();
		return parent && parent.getAbsCoordOfMode && parent.getAbsCoordOfMode(coordMode, allowCoordBorrow);
	},
	/** @private */
	_coordStickTargetChanged: function(oldValue, newValue)
	{
		if (oldValue && !newValue)  // when set target to null, copy the coords to store fields
		{
			this._copyAbsCoordFromStickedTarget(oldValue, this.getCoordParent());
		}
		else if (newValue)   // when set new target, copy the coords also
		{
			this._copyAbsCoordFromStickedTarget(newValue, this.getCoordParent());
		}
		if (oldValue && oldValue.detachCoordStickNodes)
		{
			oldValue.detachCoordStickNodes(this);
		}
		if (newValue && newValue.attachCoordStickNodes)
			newValue.attachCoordStickNodes(this);
		this.notifyCoordStickTargetChanged(oldValue, newValue);
	},

	/** @private */
	_copyAbsCoordFromStickedTarget: function(target, parent, coordMode)
	{
		var CU = Kekule.CoordUtils;
		var CM = Kekule.CoordMode;

		if (!parent)
			parent = this.getCoordParent();

		var coord2D = (!coordMode || coordMode === CM.COORD2D)? target.getAbsCoord2D(): null;
		var coord3D = (!coordMode || coordMode === CM.COORD2D)? target.getAbsCoord3D(): null;

		if (parent)
		{
			if (coord2D)
			{
				var pCoord2D = parent.getAbsCoord2D && parent.getAbsCoord2D();
				if (pCoord2D)
					coord2D = CU.substract(coord2D, pCoord2D);
			}
			if (coord3D)
			{
				var pCoord3D = parent.getAbsCoord3D && parent.getAbsCoord3D();
				if (pCoord3D)
					coord3D = CU.substract(coord3D, pCoord3D);
			}
		}
		// update stored coord field
		if (coord2D)
			this.setPropStoreFieldValue('coord2D', coord2D);
		if (coord3D)
			this.setPropStoreFieldValue('coord3D', coord3D);
	},

	// override coord getters, returns the coord of target
	// (the setter should not be overrided here)
	/** @ignore */
	doGetCoord2D: function(/*$super, */allowCoordBorrow, allowCreateNew)
	{
		var n = this.getCoordStickTarget();
		if (n && n.getAbsCoord2D)
		{
			var result = n.getAbsCoord2D(allowCoordBorrow, allowCreateNew);
			var pCoord = this._getParentAbsCoord(Kekule.CoordMode.COORD2D, allowCoordBorrow);
			if (pCoord)
				result = Kekule.CoordUtils.substract(result, pCoord);
			return result;
		}
		else
			return this.tryApplySuper('doGetCoord2D', [allowCoordBorrow, allowCreateNew])  /* $super(allowCoordBorrow, allowCreateNew) */;
	},
	/** @ignore */
	doGetCoord3D: function(/*$super, */allowCoordBorrow, allowCreateNew)
	{
		var n = this.getCoordStickTarget();
		if (n && n.getAbsCoord3D)
		{
			var result = n.getAbsCoord3D(allowCoordBorrow, allowCreateNew);
			var pCoord = this._getParentAbsCoord(Kekule.CoordMode.COORD3D, allowCoordBorrow);
			if (pCoord)
				result = Kekule.CoordUtils.substract(result, pCoord);
			return result;
		}
		else
			return this.tryApplySuper('doGetCoord3D', [allowCoordBorrow, allowCreateNew])  /* $super(allowCoordBorrow, allowCreateNew) */;
	},
	/**
	 * Returns whether this node is the only child(node or connector) in parent structure, or has no parent structure.
	 * The orphan node may be occurs in 2D molecule CH4, BH3, etc., or metal molecule.
	 * @returns {Bool}
	 */
	isOrphan: function()
	{
		var parent = this.getParent();
		return (!parent || parent.getChildCount() <= 1);
	}
});

/**
 * Enumeration of stereo parity of node or connector.
 * @enum
 */
Kekule.StereoParity = {
	NONE: null,
	ODD: 1,
	EVEN: 2,
	UNKNOWN: 0
};

/**
 * Represent an abstract structure node (atom, atom group, etc.).
 * @class
 * @augments Kekule.BaseStructureNode
 * @param {String} id Id of this node.
 * @param {Hash} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Hash} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Float} charge Charge of atom. As there may be partial charge on atom, so a float value is used.
 * @property {Int} radical Radical state of node, value should from {@link Kekule.RadicalOrder}.
 * @property {Int} parity Stereo parity of node if the node is a chiral one, following the MDL convention.
 * //@property {Array} linkedChemNodes Neighbor nodes linked to this node through proper connectors.
 * @property {Bool} isAnchor Whether this node is among anchors in parent structure.
 */
Kekule.ChemStructureNode = Class.create(Kekule.BaseStructureNode,
/** @lends Kekule.ChemStructureNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructureNode',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
		if (coord2D)
			this.setCoord2D(coord2D);
		if (coord3D)
			this.setCoord3D(coord3D);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('charge', {'dataType': DataType.FLOAT,
			'getter': function() { return this.getPropStoreFieldValue('charge') || 0; }
		});
		this.defineProp('radical', {'dataType': DataType.INT});
		this.defineProp('parity', {'dataType': DataType.INT});
		this.defineProp('isAnchor', {'dataType': DataType.BOOL, 'serializable': false,
			'getter': function()
			{
				var p = this.getParent();
				if (p && p.indexOfAnchorNode)
					return p.indexOfAnchorNode(this) >= 0;
				else
					return false;
			},
			'setter': function(value)
			{
				if (value !== this.getIsAnchor())
				{
					var p = this.getParent();
					if (p)
					{
						if (value && p.appendAnchorNode())
							p.appendAnchorNode(this);
						else if (!value && p.removeAnchorNode)
							p.removeAnchorNode(this);
					}
				}
			}
		});
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['charge', 'radical']);
	},
	/**
	 * Returns a label that represents current node.
	 * Desendants should override this method.
	 * @returns {String}
	 */
	getLabel: function()
	{
		return null;
	},

	/** @private */
	getAcceptCoordStickFrom: function(fromObj)
	{
		return (!this.isSiblingWith(fromObj) && !(fromObj instanceof Kekule.ChemStructureNode));
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'charge'))
				result.push('charge');
			if (this._getComparisonOptionFlagValue(options, 'radical'))
				result.push('radical');
			/*
			if (this._getComparisonOptionFlagValue(options, 'stereo'))
				result.push('parity');
			*/
		}
		return result;
	},
	/** @ignore*/
	doCompareProperty: function(targetObj, propName, options)
	{
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE && (propName === 'charge' || propName === 'radical'))
		{
			var v2 = (targetObj.getPropValue && targetObj.getPropValue(propName)) || 0;
			var v1 = this.getPropValue(propName) || 0;
			return v1 - v2;
		}
		else
			return this.tryApplySuper('doCompareProperty', [targetObj, propName, options]);
	},
	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)  // can not find different in $super
		{
			if (this._getComparisonOptionFlagValue(options, 'stereo'))  // parity null/0 should be regard as one in comparison
			{
				var c1 = this.getParity() || Kekule.StereoParity.UNKNOWN;
				var c2 = (targetObj.getParity && targetObj.getParity()) || Kekule.StereoParity.UNKNOWN;
				result = this.doCompareOnValue(c1, c2, options);
			}
		}
		return result;
	},

	/**
	 * Returns the most possible isotope of node.
	 * To {@link Kekule.Atom}, this should be simplely the isotope of atom
	 * while variable atom or pseudoatom may has its own implementation.
	 * This method returns null if isotope is uncertain to this node.
	 * Descendants need to override this method.
	 * @returns {Kekule.Isotope}
	 */
	getPrimaryIsotope: function()
	{
		return null;
	},
	/**
	 * Returns neighbor nodes linked to this node through proper connectors.
	 * @param {Bool} ignoreHydrogenAtoms Whether explicit hydrogen atoms are returned. Default is false.
	 * @return {Array}
	 */
	getLinkedChemNodes: function(ignoreHydrogenAtoms)
	{
		var linkedObjs = this.getLinkedObjs();
		var result = [];
		for (var i = 0, l = linkedObjs.length; i < l; ++i)
		{
			var obj = linkedObjs[i];
			if (obj instanceof Kekule.ChemStructureNode && (!ignoreHydrogenAtoms || !obj.isHydrogenAtom || !obj.isHydrogenAtom()))
				result.push(obj);
		}
		return result;
	},
	/**
	 * Returns linked instances of {@link Kekule.Bond} to this node.
	 * @param {Int} bondType
	 * @returns {Array}
	 */
	getLinkedBonds: function(bondType)
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var c = this.getLinkedConnectorAt(i);
			if ((c instanceof Kekule.Bond) && (!bondType || c.getBondType() === bondType))
				result.push(c);
		}
		return result;
	},
	/**
	 * Returns linked multicenter bonds to this node.
	 * @returns {Array}
	 */
	getLinkedMultiCenterBonds: function()
	{
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var c = this.getLinkedConnectorAt(i);
			if ((c instanceof Kekule.Bond) && (c.getConnectedObjCount() > 2))
			{
				result.push(c);
			}
		}
		return result;
	},
	/**
	 * Returns linked multiple covalent bonds to this node.
	 * @returns {Array}
	 */
	getLinkedMultipleBonds: function()
	{
		var BO = Kekule.BondOrder;
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var c = this.getLinkedConnectorAt(i);
			if ((c instanceof Kekule.Bond) && (c.getBondType() === Kekule.BondType.COVALENT))
			{
				var bondOrder = c.getBondOrder();
				if ((bondOrder === BO.DOUBLE) || (bondOrder === BO.TRIPLE) || (bondOrder === BO.QUAD) || (bondOrder === BO.EXPLICIT_AROMATIC))
					result.push(c);
			}
		}
		return result;
	},
	/**
	 * Returns linked double covalent bond to this node.
	 * @returns {Array}
	 */
	getLinkedDoubleBonds: function()
	{
		var BO = Kekule.BondOrder;
		var result = [];
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var c = this.getLinkedConnectorAt(i);
			if ((c instanceof Kekule.Bond) && (c.getBondType() === Kekule.BondType.COVALENT))
			{
				var bondOrder = c.getBondOrder();
				if (bondOrder === BO.DOUBLE)
					result.push(c);
			}
		}
		return result;
	},

	/** @ignore */
	clearStructureFlags: function()
	{
		//this.setParity(Kekule.StereoParity.NONE);
		this.setPropStoreFieldValue('parity', Kekule.StereoParity.NONE);  // avoid invoke change event
	},

	/**
	 * Returns whether this node is a H atom (but not D or T).
	 * @returns {Bool}
	 */
	isHydrogenAtom: function()
	{
		return false;
	},

	/** @private */
	_getBondsValenceInfo: function(connectors)
	{
		var valenceSum = 0;
		var maxValence = 0;
		var piECount = this.getStructureCacheData('piElectronCount') || null;
		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			var connector = connectors[i];
			// check if connector is a covalance bond
			if (connector instanceof Kekule.Bond)
			{
				var v;
				// a fix, if a hetero atom connected with two explicit aromatics bonds in a aromatic ring (e.g., pyrole),
				// and the pi electron count is 2 (rather than 1), the bond order should be considered as 1 rather than 2
				if (connector.getBondType() == Kekule.BondType.COVALENT && connector.getBondOrder() === Kekule.BondOrder.EXPLICIT_AROMATIC && piECount >= 2)
					v = 1;
				else
					v = connector.getBondValence();
				valenceSum += v;
				if (v > maxValence)
					maxValence = v;
			}
		}
		return {'valenceSum': valenceSum, 'maxValence': maxValence};
	},

	/** @private */
	_getCurrCovalentBondsInfo: function()
	{
		/*
		var valenceSum = 0;
		var maxValence = 0;
		var piECount = this.getStructureCacheData('piElectronCount') || null;
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			// check if connector is a covalance bond
			if ((connector instanceof Kekule.Bond)
				&& (connector.getBondType() == Kekule.BondType.COVALENT))
			{
				var v;
				if (filter && !filter(connector))
					continue;
				// a fix, if a hetero atom connected with two explicit aromatics bonds in a aromatic ring (e.g., pyrole),
				// and the pi electron count is 2 (rather than 1), the bond order should be considered as 1 rather than 2
				if (connector.getBondOrder() === Kekule.BondOrder.EXPLICIT_AROMATIC && piECount >= 2)
					v = 1;
				else
					v = connector.getBondValence();
				valenceSum += v;
				if (v > maxValence)
					maxValence = v;
			}
		}
		return {'valenceSum': valenceSum, 'maxValence': maxValence};
		*/
		return this._getBondsValenceInfo(this.filterConnector(function(connector){
			return (connector instanceof Kekule.Bond) && (connector.getBondType() == Kekule.BondType.COVALENT);
		}));
	},
	/** @private */
	_getCurrIonicBondsInfo: function(filter)
	{
		/*
		var valenceSum = 0;
		var maxValence = 0;
		for (var i = 0, l = this.getLinkedConnectorCount(); i < l; ++i)
		{
			var connector = this.getLinkedConnectorAt(i);
			// check if connector is a covalance bond
			if ((connector instanceof Kekule.Bond)
				&& (connector.getBondType() == Kekule.BondType.IONIC))
			{
				if (filter && !filter(connector))
					continue;
				var v = connector.getBondValence();
				valenceSum += v;
				if (v > maxValence)
					maxValence = v;
			}
		}
		return {'valenceSum': valenceSum, 'maxValence': maxValence};
		*/
		return this._getBondsValenceInfo(this.filterConnector(function(connector){
			return (connector instanceof Kekule.Bond) && (connector.getBondType() == Kekule.BondType.IONIC);
		}));
	}
});

/*
Kekule.RadicalType = {
	NONE: 0,
	SINGLET: 1,
	DOUBLET: 2,
	TRIPLET: 3
};
*/
/** @deprected */
Kekule.RadicalType = Kekule.RadicalOrder;   /* A duplicate definition, for backward compatity. */

/**
 * Represent an abstract atom, parent for dummy atom, concrete atom or variable atom.
 * @class
 * @augments Kekule.ChemStructureNode
 * @param {String} id Id of this node.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Int} explicitHydrogenCount Explicit hydrogen count on atom.
 */
Kekule.AbstractAtom = Class.create(Kekule.ChemStructureNode,
/** @lends Kekule.AbstractAtom# */
{
	/** @private */
	CLASS_NAME: 'Kekule.AbstractAtom',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id, coord2D, coord3D])  /* $super(id, coord2D, coord3D) */;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('explicitHydrogenCount', {
			'dataType': DataType.INT, 'scope': Class.PropertyScope.PUBLISHED,
			'setter': function(value) {
				var fValue = parseFloat(value);
				var v = (isNaN(fValue) || fValue < 0)? void(0): fValue;
				this.setPropStoreFieldValue('explicitHydrogenCount', v);
			}
		});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'a';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['explicitHydrogenCount']);
	},

	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'hydrogenCount'))
			{
				var c1 = this.getHydrogenCount(true);
				var c2 = targetObj.getHydrogenCount && targetObj.getHydrogenCount(true);
				result = this.doCompareOnValue(c1, c2, options);
			}
		}
		return result;
	},

	/**
	 * Returns hydrogen count linked to this atom.
	 * Same as getExplicitHydrogenCount if includingBondedHydrogen is false.
	 * @param {Bool} includingBondedHydrogen If true, hydrogen siblings linked with single bond will also be take into consideration.
	 */
	getHydrogenCount: function(includingBondedHydrogen)
	{
		var result = this.getExplicitHydrogenCount() || 0;
		if (includingBondedHydrogen)
		{
			//var hatoms = this.getLinkedHydrogenAtoms();
			/*
			var hatoms = this.getLinkedHydrogenAtomsWithSingleBond();
			result += hatoms.length || 0;
			var cachedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
			result += cachedBondHydrogenCount;
			*/
			result += this.getLinkedHydrogenAtomsWithSingleBondCount(false);
		}
		// the bond of cached bonded Hs are already be removed, so thet need to be calculated too
		var cachedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
		result += cachedBondHydrogenCount;
		return result;
	},
	/**
	 * Same as setExplicitHydrogenCount.
	 * @param {Int} value
	 */
	setHydrogenCount: function(value)
	{
		return this.setExplicitHydrogenCount(value);
	},
	/**
	 * Returns whether the explicitHydrogen property is set.
	 * @returns {Bool}
	 */
	hasExplicitHydrogens: function()
	{
		return Kekule.ObjUtils.notUnset(this.getExplicitHydrogenCount());
	},

	/**
	 * Returns whether this atom is a saturated one.
	 * @returns {Bool}
	 */
	isSaturated: function()
	{
		return !this.getLinkedMultipleBonds().length;
	},

	/**
	 * Returns when this node is an atom of certain element or
	 * maybe or may include element (peusdo atom or atom list).
	 * @param {Variant} atomicNumberOrSymbol
	 * @returns {Bool}
	 */
	mayContainElement: function(atomicNumberOrSymbol)
	{
		var num;
		if (typeof(atomicNumberOrSymbol) === 'string')  // symbol
			num = Kekule.ChemicalElementsDataUtil.getAtomicNumber(atomicNumberOrSymbol);
		else
			num = atomicNumberOrSymbol;
		return this.doMayContainElement(num);
	},
	/**
	 * Do actual judge of method mayContainElement. Descendants need to override this method.
	 * @param {Int} atomicNum
	 * @returns {Bool}
	 */
	doMayContainElement: function(atomicNum)
	{
		return false;
	}
});

/**
 * Represent an atom in chemical structure.
 * @class
 * @augments Kekule.AbstractAtom
 * @param {String} id Id of this node.
 * @param {Variant} elemSymbolOrAtomicNumber Element symbol (String) or atomic number (Int) of atom.
 * @param {Int} massNumber Isotope mass number of atom, can be null.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Kekule.Isotope} isotope The isotope and element of atom.
 * @property {String} symbol The element symbol of atom.
 * @property {Int} atomicNumber The atomic number symbol of atom.
 * @property {Int} massNumber The isotope mass number symbol of atom.
 * @property {Hash} atomType The type if this atom, data is read from {@link kekule.structGenAtomTypesData.js}.
 *   Undefined or null means uncertain type.
 * @property {Int} hybridizationType Hybridization type (sp/sp2/sp3) of atom Undefined or null means uncertain type.
 * @property {Bool} disableImplicitHydrogenEstimation Whether the auto calcualtion of implicit H count is disabled (e.g., for metal atoms).
 */
Kekule.Atom = Class.create(Kekule.AbstractAtom,
/** @lends Kekule.Atom# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Atom',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id, coord2D, coord3D])  /* $super(id, coord2D, coord3D) */;
		if (elemSymbolOrAtomicNumber || massNumber)
			this.changeIsotope(elemSymbolOrAtomicNumber, massNumber);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('isotope', {'dataType': 'Kekule.Isotope', 'serializable': false});
		this.defineProp('isotopeId', {
			'dataType': DataType.STRING,
			'serializable': true,
			'getter': function()
				{
					var i = this.getIsotope();
					return i? i.getIsotopeId(): Kekule.Element.UNSET_ELEMENT;
				},
			'setter': function(value)
				{
					var newIsotope = Kekule.IsotopeFactory.getIsotopeById(value);
					//this.changeElement(value);
					if (newIsotope)
						this.setIsotope(newIsotope);
				}
		});
		this.defineProp('symbol', {
			'dataType': DataType.STRING,
			'serializable': false,
			'getter': function()
				{
					var i = this.getIsotope();
					var result = i? i.getSymbol(): Kekule.Element.UNSET_ELEMENT;
					return result;
				},
			'setter': function(value) { this.changeElement(value); }
		});
		this.defineProp('atomicNumber', {
			'dataType': DataType.INTEGER,
			'serializable': false,
			'getter': function()
				{
					var i = this.getIsotope();
					return i? i.getAtomicNumber(): 0;
				},
			'setter': function(value)
				{
					this.changeElement(value);
				}
		});
		this.defineProp('massNumber', {
			'dataType': DataType.INTEGER,
			'getter': function()
				{
					var i = this.getIsotope();
					return i? i.getMassNumber(): Kekule.Isotope.UNSET_MASSNUMBER;
				},
			'setter': function(value)
				{
					this.changeMassNumber(value);
				}
		});
		this.defineProp('isotopeAlias', {
			'dataType': DataType.STRING,
			'getter': function()
			{
				var i = this.getIsotope();
				return i? i.getIsotopeAlias(): undefined;
			},
			'setter': function(value)
			{
				this.changeElement(value);
			}
		});
		this.defineProp('atomType', {
			'dataType': DataType.OBJECT,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC
			/*,
			'getter': function()
				{
					var result = this.getPropStoreFieldValue('atomType');
					return result? result: this.guessAtomType();
				}*/
		});
		// a property for persistent atomType, should not use it directly
		this.defineProp('atomTypeId', {
			'dataType': DataType.STRING,
			'getter': function()
				{
					var atype = this.getPropStoreFieldValue('atomType');
					return atype? atype.id: Kekule.AtomType.UNSET_ATOMTYPE;
				},
			'setter': function(value)
				{
					if (this.isNormalAtom())
					{
						var atype = Kekule.AtomTypeDataUtil.getAtomTypeFromId(this.getAtomicNumber(), value);
						this.setAtomType(atype);
					}
				}
		});
		this.defineProp('hybridizationType', {'dataType': DataType.INT, 'enumSource': Kekule.HybridizationType});
		this.defineProp('disableImplicitHydrogenEstimation', {'dataType': DataType.BOOL});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'a';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['isotope', 'atomType']);
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'mass'))
				result.unshift('massNumber');
			if (this._getComparisonOptionFlagValue(options, 'atom'))
				result.unshift('atomicNumber');   // atom number is the most important one, must be at the beginning
		}
		return result;
	},

	/** @ignore */
	getPrimaryIsotope: function()
	{
		return this.getIsotope();
	},
	/** @ignore */
	getLabel: function()
	{
		return '' + (this.getMassNumber() || '') + this.getSymbol();
	},
	/** @ignore */
	doMayContainElement: function(atomicNum)
	{
		return this.getAtomicNumber() === atomicNum;
	},

	/**
	 * Change atom isotope to new symbol / atmoic number or new mass number
	 * @param {Variant} symbolOrAtomicNumber Symbol(String) or atomic number(Int) of element.
	 *   If undefined is assigned, the old atomic number will be used.
	 * @param {Int} massNumber Mass number of isotope.
	 */
	changeIsotope: function(symbolOrAtomicNumber, massNumber)
	{
		if (symbolOrAtomicNumber === Kekule.Element.UNSET_ELEMENT)
			massNumber = Kekule.Isotope.UNSET_MASSNUMBER;
		else if (!symbolOrAtomicNumber)
			symbolOrAtomicNumber = this.getAtomicNumber();

		if (!massNumber)
			massNumber = Kekule.Isotope.UNSET_MASSNUMBER;

		if (this.getAtomicNumber() !== symbolOrAtomicNumber)  // element change
			this.setAtomType(Kekule.AtomType.UNSET_ATOMTYPE);

		var isotope = Kekule.IsotopeFactory.getIsotope(symbolOrAtomicNumber, massNumber);
		//this.setPropStoreFieldValue('isotope', isotope);
		this.setIsotope(isotope);
	},
	/**
	 * Change atom isotope to new symbol / atmoic number
	 * @param {Variant} symbolOrAtomicNumber Symbol(String) or atomic number(Int) of element.
	 */
	changeElement: function(symbolOrAtomicNumber)
	{
		return this.changeIsotope(symbolOrAtomicNumber, Kekule.Isotope.UNSET_MASSNUMBER);
	},
	/**
	 * Change atom mass number. The element will remains the same.
	 * @param {Int} massNumber Mass number of isotope.
	 */
	changeMassNumber: function(massNumber)
	{
		return this.changeIsotope(null, massNumber);
	},

	/**
	 * Check if current atom is a certain element.
	 * @param {Variant} symbolOrAtomicNumber Symbol(String) or atomic number(Int) of element.
	 */
	isElement: function(symbolOrAtomicNumber)
	{
		return (this.getSymbol() === symbolOrAtomicNumber) || (this.getAtomicNumber() === symbolOrAtomicNumber);
	},

	/**
	 * Returns the element series of current atom.
	 * @returns {String} Value from {@link Kekule.ElementSeries}.
	 */
	getElementSeries: function()
	{
		var isotope = this.getIsotope();
		return isotope && isotope.getSeries();
	},
	/**
	 * Check if this is a normal atom (not a pseudo one or unset one)
	 */
	isNormalAtom: function()
	{
		var isotope = this.getIsotope();
		return isotope? isotope.isNormalElement(): false;
	},
	/** @ignore */
	isHydrogenAtom: function()
	{
		return this.isElement(1) && (!this.getMassNumber() || this.getMassNumber() <= 1);
	},

	/**
	 * If the atomType property is not set explicitly, use this function to guess the atom type.
	 * returns {Object} Atom type JSON object from {@link kekule.structGenAtomTypesData.js}
	 */
	guessAtomType: function()
	{
		if (this.isNormalAtom())
		{
			var possible = null, fallback = null;
			// get current bond order sum first
			var bondsInfo = this._getCurrCovalentBondsInfo();
			// all possible atom types
			var atomTypes = Kekule.AtomTypeDataUtil.getAllAtomTypes(this.getAtomicNumber());
			// as we already know that the atomTypes are sorted by bondOrderSum / maxBondOrder
			if (atomTypes)
			{
				for (var i = 0, l = atomTypes.length; i < l; ++i)
				{
					var atomType = atomTypes[i];
					fallback = atomType;
					if (atomType.bondOrderSum >= bondsInfo.valenceSum)
					{
						possible = atomType;
						if (atomType.maxBondOrder >= bondsInfo.maxValence)
						{
							return atomType;
						}
					}
				}
				// not found a suitable one
				return possible ? possible : fallback;
			}
		}
		return null;
	},

	/**
	 * Returns explicit atom type when property {@link Kekule.Atom@atomType} is set, or a guessed atom type.
	 * @returns {Hash}  Atom type JSON object from {@link kekule.structGenAtomTypesData.js}
	 */
	getProximalAtomType: function()
	{
		return this.getAtomType() || this.guessAtomType();
	},

	/**
	 * Guess and returns the implicit valence of atom.
	 * Note this method does not consider the explicit hydrogen count.
	 * @returns {Int}
	 */
	getImplicitValence: function()
	{
		if (this.isNormalAtom())
		{
			var bondsInfo = this._getCurrCovalentBondsInfo();
			var expValence = bondsInfo.valenceSum;
			var charge = Math.round(this.getCharge() || 0);

			var result = Kekule.ValenceUtils.getImplicitValence(this.getAtomicNumber(), charge, expValence);

			return result;
		}
		else
			return 0;
	},
	/**
	 * Returns the valence of all explicit factors (including bond and explicit hydrogens) of atom.
	 * @param {Hash} options May include the following fields:
	 * {
	 *   ignoreBondHydrogens: Bool,
	 *   ignoreExplicitHydrogens: Bool,
	 * }
	 * @returns {Int}
	 */
	getExplicitValence: function(options)
	{
		var ops = options || {};
		if (this.isNormalAtom())
		{
			var bondsInfo = this._getCurrCovalentBondsInfo();
			var expValence = bondsInfo.valenceSum;
			if (!ops.ignoreBondHydrogens)
			{
				var omittedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
				expValence += omittedBondHydrogenCount;
			}
			else  // ignore bond hydrogens
			{
				expValence -= (this.getLinkedHydrogenAtomsWithSingleBond() || []).length;
			}
			// adjust with explicitHCount
			if (ops.ignoreExplicitHydrogens)
			{

			}
			else
			{
				var explicitHCount = this.getExplicitHydrogenCount() || 0;
				expValence += explicitHCount;
			}

			return expValence;
		}
		else
			return 0;
	},
	/**
	 * Guess and returns the valence of atom.
	 * Note this method considers the explicit hydrogen count.
	 * @param {Hash} options May include the following fields:
	 * {
	 *   ignoreBondHydrogens: Bool,
	 *   ignoreExplicitHydrogens: Bool,
	 *   ignoreCharge: Bool
	 * }
	 * @returns {Int}
	 */
	getValence: function(options)
	{
		var ops = options || {};
		if (this.isNormalAtom())
		{
			/*
			var bondsInfo = this._getCurrCovalentBondsInfo();
			var expValence = bondsInfo.valenceSum;
			if (!ops.ignoreBondHydrogens)
			{
				var omittedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
				expValence += omittedBondHydrogenCount;
			}
			else  // ignore bond hydrogens
			{
				expValence -= (this.getLinkedHydrogenAtomsWithSingleBond() || []).length;
			}
			// adjust with explicitHCount
			if (ops.ignoreExplicitHydrogens)
			{

			}
			else
			{
				var explicitHCount = this.getExplicitHydrogenCount() || 0;
				expValence += explicitHCount;
			}
			*/
			var result;
			var expValence = this.getExplicitValence(ops);
			if (!this.hasExplicitHydrogens())  // has no explicit hydrogen set, now the implicit hydrogen should be considered
			{
				var charge = ops.ignoreCharge? 0: Math.round(this.getCharge() || 0);
				result = Kekule.ValenceUtils.getImplicitValence(this.getAtomicNumber(), charge, expValence);
			}
			else   // no implicit hydrogen, returns the explicitValence directly
				result = expValence;
			return result;
		}
		else
			return 0;
	},

	/**
	 * Returns the count of implicit hydrogens.
	 * Note when calculating the implicit H count, explicit hydrogens are automatically ignored.
	 * //@param {Hash} options Options to calculate implicit H count. It may including the following fields: <br />
	 * //  allowNegative: Bool. When true, a negative value may be returned when bond order sum and explicit H count is too large.
	 *
	 * @returns {Int} Implicit hydrogen count.
	 */
	getImplicitHydrogenCount: function(options)
	{
		if (this.isNormalAtom() && this._needToEsitmateImplicityHydrogens())
		{
			//if (Kekule.ObjUtils.isUnset(this.getExplicitHydrogenCount()))
			{
				//var allowNegative = options && options.allowNegative;
				var coValentBondsInfo = this._getCurrCovalentBondsInfo();
				var ionicBondsInfo = this._getCurrIonicBondsInfo();
				/*
				var atype = this.getProximalAtomType();
				var valence = atype? atype.bondOrderSum: 0;
				var charge = Math.round(this.getCharge() || 0);
				*/

				var charge = Math.round(this.getCharge() || 0);
				var radicalECount = Kekule.RadicalOrder.getRadicalElectronCount(this.getRadical());
				var omittedBondHydrogenCount = this.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
				//var explicitBondedHydrogenAtoms = ((this.getLinkedHydrogenAtoms && this.getLinkedHydrogenAtoms()) || []).length;
				//var coValenceBondValuenceSum
				/*
				var valence = this.getImplicitValence();
				// adjust with radical
				valence -= radicalECount;

				// DONE: some atoms such as C should be treat differently, as C+ can only link 3 bonds
				return Math.max(valence - coValentBondsInfo.valenceSum - ionicBondsInfo.valenceSum, 0);
				*/
				var result = Kekule.ChemStructureUtils.getImplicitHydrogenCount(this.getAtomicNumber() || 0, {
					'coValenceBondValenceSum': (coValentBondsInfo.valenceSum || 0) + omittedBondHydrogenCount,
					'otherBondValenceSum': ionicBondsInfo.valenceSum || 0,
					'charge': charge,
					'radicalECount': radicalECount
					//'allowNegative': allowNegative
				});
				return result;
			}
		}
		else
			return 0;
	},
	/** @private */
	_needToEsitmateImplicityHydrogens: function()
	{
		var result = !this.getDisableImplicitHydrogenEstimation();
		if (result)
		{
			var isOrphan = this.isOrphan();
			var symbol = this.getSymbol();
			var strategyRoot = Kekule.globalOptions.get('structure.implicitHydrogenEstimationStrategy') || {};
			// check element
			if ((strategyRoot.targetElements && strategyRoot.targetElements[symbol] === false)
				|| (isOrphan && strategyRoot.targetOrphanAtomElements && strategyRoot.targetOrphanAtomElements[symbol] === false))
			{
				result = false;
			}
			// check element category
			if (result)
			{
				var targetCategories = strategyRoot.targetElementCategories;
				var targetOrphanCategories = strategyRoot.targetOrphanAtomElementCategories;
				var elemCategories = this.getIsotope().getCategories();
				for (var i = 0, l = elemCategories.length; i < l; ++i)
				{
					var c = elemCategories[i];
					if ((targetCategories && targetCategories[c] === false)
						|| (isOrphan && targetOrphanCategories && targetOrphanCategories[c] === false))
					{
						result = false;
						break;
					}
				}
			}
		}
		return result;
	},

	/**
	 * Returns the single bonded hydrogen atoms + explicit or implicit hydrogens count.
	 * @param {Bool} includingBondedHydrogen If true, hydrogen siblings linked with single bond will also be take into consideration.
	 */
	getHydrogenCount: function(includingBondedHydrogen)
	{
	  /*
		var result;
		if (Kekule.ObjUtils.isUnset(this.getExplicitHydrogenCount()))
			result = this.getImplicitHydrogenCount() || 0;
		else
			result = this.getExplicitHydrogenCount() || 0;
		if (includingBondedHydrogen)
		{
			result += this.getLinkedHydrogenAtomsWithSingleBondCount(true);
		}
		return result;
	  */
    var result = this.tryApplySuper('getHydrogenCount', [includingBondedHydrogen]);
    if (Kekule.ObjUtils.isUnset(this.getExplicitHydrogenCount()))  // when explicit hydrogen is not set, we need to calc the implicit ones
      result += this.getImplicitHydrogenCount() || 0;
    return result;
	},
	/**
	 * Returns exact mass of current atom.
	 * @returns {Float}
	 */
	getAtomicMass: function()
	{
		var result = null;
		var isotope = this.getIsotope();
		if (isotope)
		{
			result = isotope.getExactMass() || isotope.getNaturalMass();
		}
		return result;
	}
});

/**
 * Enumeration of unreal atom types.
 * @class
 */
Kekule.PseudoatomType = {
	/** A dummy atom. */
	DUMMY: 'dummy',
	/** Unspecific atom. */
	ANY: 'any',
	/** Non C/H atom. */
	HETERO: 'hetero',
	/** User custom symbol */
	CUSTOM: 'custom'
};

/**
 * Represent an unreal atom, such as dummy atom (A point or object with no chemical semantics), a "any atom" and so on.
 * Examples can be centroids, bond-midpoints, orienting "atoms" in small z-matrices.
 * @class
 * @augments Kekule.AbstractAtom
 * @param {String} id Id of this node.
 * @param {String} atomType Unreal atom type, value from {@link Kekule.PseudoatomType}.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {String} atomType Unreal atom type, value from {@link Kekule.PseudoatomType}.
 * @property {String} symbol User custom symbol. Meanless if atomType != Kekule.PseudoatomType.CUSTOM.
 */
Kekule.Pseudoatom = Class.create(Kekule.AbstractAtom,
/** @lends Kekule.Pseudoatom# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Pseudoatom',
	/** @constructs */
	initialize: function(/*$super, */id, atomType, symbol, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id, coord2D, coord3D])  /* $super(id, coord2D, coord3D) */;
		if (atomType)
			this.setAtomType(atomType);
		if (symbol && (atomType == Kekule.PseudoatomType.CUSTOM))
			this.setSymbol(symbol);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('atomType', {'dataType': DataType.STRING, 'enumSource': Kekule.PseudoatomType});
		this.defineProp('symbol', {'dataType': DataType.STRING,
			'getter': function()
			{
				var t = this.getAtomType();
				var PT = Kekule.PseudoatomType;
				var NL = Kekule.ChemStructureNodeLabels;
				return (t === PT.DUMMY)? NL.DUMMY_ATOM:
					(t === PT.ANY)? NL.ANY_ATOM:
					(t === PT.HETERO)? NL.HETERO_ATOM:
					this.getPropStoreFieldValue('symbol') || NL.CUSTOM_ATOM;
			},
			'setter': function(value)
			{
				var PT = Kekule.PseudoatomType;
				var NL = Kekule.ChemStructureNodeLabels;
				if (value)
				{
					var t = (value === NL.DUMMY_ATOM) ? PT.DUMMY :
						(value === NL.ANY_ATOM) ? PT.ANY :
						(value === NL.HETERO_ATOM) ? PT.HETERO :
						PT.CUSTOM;
					this.setAtomType(t);
				}
				this.setPropStoreFieldValue('symbol', value);
			}
		});
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['atomType', 'symbol']);
	},
	/** @ignore */
	getPrimaryIsotope: function()
	{
		return null;
	},
	/** @ignore */
	getLabel: function()
	{
		return this.getSymbol();
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'atom'))
				result.push('atomType');
		}
		return result;
	},
	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'atom'))
			{
				if (this.getAtomType() === Kekule.PseudoatomType.CUSTOM)  // custom pseudo atom, need to check symbol further
				{
					var v1 = this.getSymbol();
					var v2 = targetObj.getSymbol && targetObj.getSymbol();
					result = this.doCompareOnValue(v1, v2, options);
				}
			}
		}
		return result;
	},

	/** @ignore */
	doMayContainElement: function(atomicNum)
	{
		var PT = Kekule.PseudoatomType;
		var t = this.getAtomType();
		if (t === PT.ANY)
			return true;
		else if (t === PT.HETERO)
		{
			if ([1, 6].indexOf(atomicNum) >= 0)  // C/H not hetero
				return false;
			else
			{
				var elemInfo = Kekule.ChemicalElementsDataUtil.getElementInfo(atomicNum);
				return (elemInfo.chemicalSerie === "Nonmetals");
			}
		}
		else // dummy or custom
			return false;
	}
});

/**
 * Represent an variable atom,  In this type of atom, element/isotope can be one of a range.
 *   (such as atomlist in MDL CTAB).
 *
 * @class
 * @augments Kekule.AbstractAtom
 * @param {String} id Id of this node.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Array} allowedIsotopeIds Atom isotope may vary in this array.
 * @property {Array} disallowedIsotopeIds Atom isotope must not in the array.
 *   if {@link Kekule.VariableAtom#allowedIsotopeIds} is set, this property is ignored.
 */
Kekule.VariableAtom = Class.create(Kekule.AbstractAtom,
/** @lends Kekule.VariableAtom# */
{
	/** @private */
	CLASS_NAME: 'Kekule.VariableAtom',
	/** @private */
	initProperties: function()
	{
		this.defineProp('allowedIsotopeIds', {
			'dataType': DataType.ARRAY,
			'getter': function(canCreate)
				{
					var r = this.getPropStoreFieldValue('allowedIsotopeIds');
					if ((!r) && canCreate)
					{
						r = [];
						this.setPropStoreFieldValue('allowedIsotopeIds', r);
					}
					return r;
				}
		});
		this.defineProp('disallowedIsotopeIds', {
			'dataType': DataType.ARRAY,
			'getter': function(canCreate)
				{
					var r = this.getPropStoreFieldValue('disallowedIsotopeIds');
					if ((!r) && canCreate)
					{
						r = [];
						this.setPropStoreFieldValue('disallowedIsotopeIds', r);
					}
					return r;
				}
		});
		this.defineProp('allowedIsotopes', {
			'dataType': DataType.ARRAY,
			'setter': null,
			'getter': function()
			{
				return this._getIsotopesFromIds(this.getAllowedIsotopeIds());
			}
		});
		this.defineProp('disallowedIsotopes', {
			'dataType': DataType.ARRAY,
			'setter': null,
			'getter': function()
			{
				return this._getIsotopesFromIds(this.getDisallowedIsotopeIds());
			}
		});
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['allowedIsotopeIds', 'disallowedIsotopeIds']);
	},

	/** @ignore */
	getPrimaryIsotope: function()
	{
		if (this.isDisallowedList())
			return null;
		else
		{
			var ids = this.getAllowedIsotopeIds();
			var id = ids? ids[0]: null;
			return id? Kekule.IsotopeFactory.getIsotopeById(id): null;
		}
	},
	/** @ignore */
	getLabel: function()
	{
		return Kekule.ChemStructureNodeLabels.VARIABLE_ATOM;
	},

	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'atom'))
			{
				var AU = Kekule.ArrayUtils;
				var allowedIds1 = this.getAllowedIsotopeIds();
				var allowedIds2 = targetObj.getAllowedIsotopeIds && targetObj.getAllowedIsotopeIds();
				if (allowedIds1)  // allowed will override disallowed
				{
					allowedIds1 = AU.clone(allowedIds1).sort();
					if (allowedIds2 && allowedIds2.length)
						allowedIds2 = AU.clone(allowedIds2).sort();
					result = this.doCompareOnValue(allowedIds1, allowedIds2, options);
				}
				else  // consider disallowed
				{
					if (allowedIds2)  // target use allowed list
						result = 1;
					else
					{
						var disallowedIds1 = this.getDisallowedIsotopeIds();
						var disallowedIds2 = targetObj.getDisallowedIsotopeIds && targetObj.getDisallowedIsotopeIds();
						if (disallowedIds1 && disallowedIds1.length)
							disallowedIds1 = AU.clone(disallowedIds1).sort();
						if (disallowedIds2 && disallowedIds2.length)
							disallowedIds2 = AU.clone(disallowedIds2).sort();
						result = this.doCompareOnValue(disallowedIds1, disallowedIds2, options);
					}
				}
			}
		}
		return result;
	},

	/** @ignore */
	doMayContainElement: function(atomicNum)
	{
		var atomicNumInIds = function(atomicNum, ids)
		{
			if (!ids)
				return false;
			for (var i = 0, l = ids.length; i < l; ++i)
			{
				var id = ids[i];
				var d = Kekule.IsotopesDataUtil.getIsotopeIdDetail(id);
				var symbol = d.symbol;
				var num = Kekule.ChemicalElementsDataUtil.getAtomicNumber(symbol);
				if (num === atomicNum)
					return true;
			}
			return false;
		};
		var isotopeIds = this.getAllowedIsotopeIds();
		if (isotopeIds)
		{
			return atomicNumInIds(atomicNum, isotopeIds);
		}
		else
		{
			isotopeIds = this.getDisallowedIsotopeIds();
			if (isotopeIds)
				return !atomicNumInIds(atomicNum, isotopeIds);
		}
	},

	/**
	 * Check whether this list has disallowedIsotopeIds instead of allowedIsotopeIds.
	 * @returns {Bool}
	 */
	isDisallowedList: function()
	{
		return this.getDisallowedIsotopeIds() && (!this.getAllowedIsotopeIds());
	},
	/**
	 * Check if neither allowed list nor disallowed list is set.
	 */
	isListEmpty: function()
	{
		var list = this.getAllowedIsotopeIds();
		var result = !list || !list.length;
		if (result)
		{
			list = this.getDisallowedIsotopeIds();
			result = !list || !list.length;
		}
		return result;
	},

	/**
	 * Get allowedIsotopeIds array, if null, create a new array
	 * @returns {Array}
	 */
	fetchAllowedIsotopeIds: function()
	{
		return this.doGetAllowedIsotopeIds(true);
	},
	/**
	 * Get disallowedIsotopeIds array, if null, create a new array
	 * @returns {Array}
	 */
	fetchDisallowedIsotopeIds: function()
	{
		return this.doGetDisallowedIsotopeIds(true);
	},

	/** @private */
	_getIsotopesFromIds: function(ids)
	{
		if (!ids)
			return null;
		var result = [];
		for (var i = 0, l = ids.length; i < l; ++i)
		{
			var id = ids[i];
			var isotope = Kekule.IsotopeFactory.getIsotopeById(id);
			if (isotope)
				result.push(isotope);
		}
		return result;
	}
});

/**
 * A formula for representing a simple (especially inorganic) molecule or group.
 * @class
 * @augments ObjectEx
 * @param {Kekule.StructureFragment} parent Parent to hold this formula.
 *
 * @property {Kekule.StructureFragment} parent Parent to hold this Ctab. Read only.
 * @property {Float} charge Formal charge of this molecular.
 *   This charge is not on a certain atom but deployed in the whole formula.
 * @property {Int} radical Value from {@link Kekule.RadicalOrder}, radical state of this formula.
 *   Seldom used in formula.
 * @property {Array} sections Array of atom or sub formula maps in this formula.
 *   [{'obj': atom, 'count': count}, {'obj': subFormula, 'count': count}, ...]
 *   (charge is stored in atom or subFormula).
 *   where atom is a {@link Kekule.AbstractAtom} and subFormula is an instance of {@link Kekule.MolecularFormula}.
 *   For instance, [Cu(NH3)4]2+ SO42-] can be divided into two sub formulas: [Cu(NH3)4]2+ and SO42-,
 *   and [Cu(NH3)4]2+ can be further divided into two sub ones: Cu2+ and NH3
 */
Kekule.MolecularFormula = Class.create(ObjectEx,
/** @lends Kekule.MolecularFormula# */
{
	/** @private */
	CLASS_NAME: 'Kekule.MolecularFormula',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */parent)
	{
		this.tryApplySuper('initialize')  /* $super() */;
		this.setPropStoreFieldValue('sections', []);
		if (parent)
			this.setPropStoreFieldValue('parent', parent);
		this.setBubbleEvent(true);  // allow event bubble
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('parent', {
			'dataType': 'Kekule.StructureFragment', 'serializable': false, 'setter': null,
			'scope': Class.PropertyScope.PUBLIC
		});
		this.defineProp('sections', {'dataType': DataType.ARRAY, 'setter': null});
		this.defineProp('charge', {'dataType': DataType.FLOAT, 'getter': function() { return this.getPropStoreFieldValue('charge') || 0; } });
		this.defineProp('radical', {'dataType': DataType.INT, 'getter': function() { return this.getPropStoreFieldValue('radical') || 0; } });
	},
	/** @private */
	getHigherLevelObj: function()
	{
		return this.getParent();
	},
	/**
	 * Check if this formula contains no data.
	 * @returns {Bool}
	 */
	isEmpty: function()
	{
		return this.getSectionCount() <= 0;
	},

	/** @private */
	notifySectionsChanged: function()
	{
		this.notifyPropSet('sections', this.getPropStoreFieldValue('sections'));
	},

	/** @private */
	createSectionItem: function(atomOrSubFormula, count, charge)
	{
		var result = {'obj': atomOrSubFormula, 'count': count || 1/*, 'charge': charge || 0*/};
		if (charge)
			//result.charge = charge;
			atomOrSubFormula.setCharge(charge);
		if (atomOrSubFormula.setParent)
			atomOrSubFormula.setParent(this.getParent());
		return result;
	},

	/**
	 * Returns charge sum-up of this whole formula.
	 * @returns {Number}
	 */
	getTotalCharge: function()
	{
		var result = 0;
		var sections = this.getSections();
		for (var i = 0, l = sections.length; i < l; ++i)
		{
			/*
			if (sections[i].charge)
				result += sections[i].charge;
			else*/
			if (sections[i].obj.getCharge)
				result += sections[i].obj.getCharge() * (sections.count || 1);
			/*
			if (sections[i].charge)
				result += sections[i].charge;
			*/
		}
		if (this.getCharge())
			result += this.getCharge();
		return result;
	},

	/**
	 * Get index of atomOrSubFormula in sections.
	 * @param {Variant} atomOrSubFormula Instance of {@link Kekule.AbstractAtom} or {@link Kekule.MolecularFormula}
	 * @returns {Int} Index of searched object or -1 (when not found).
	 */
	indexOfObj: function(atomOrSubFormula)
	{
		var sections = this.getSections();
		for (var i = 0, l = sections.length; i < l; ++i)
		{
			if (sections[i].obj == atomOrSubFormula)
				return i;
		}
		return -1;
	},
	/**
	 * Returns the count of sections in this formula.
	 * @returns {Int}
	 */
	getSectionCount: function()
	{
		return this.getSections().length;
	},
	/**
	 * Returns a section item at index.
	 * @param {Int} index
	 * @returns {Object}
	 */
	getSectionAt: function(index)
	{
		return this.getSections()[index];
	},
	/**
	 * Get charge of a section.
	 * @param {Variant} itemOrIndex Section item or section index.
	 */
	getSectionCharge: function(itemOrIndex)
	{
		var result = 0;
		var sec;
		if (typeof(itemOrIndex) != 'object')
			sec = this.getSectionAt(itemOrIndex);
		else
			sec = itemOrIndex;
		/*
		if (sec.charge)
			result += sec.charge;
		*/
		if (sec.obj.getCharge)
			result += (sec.obj.getCharge() || 0);
		return result;
	},
	/**
	 * Append a new section.
	 * @param {Variant} atomOrSubFormula Instance of {@link Kekule.AbstractAtom} or {@link Kekule.MolecularFormula}
	 * @param {Float} count Count of added object in formula. Float value (0.5 and so on) are allowed. Default is 1.
	 * @param {Float} charge Charge of this object, can be float to indicate partial charge. Default is 0.
	 * @returns {Object} Section item added.
	 */
	appendSection: function(atomOrSubFormula, count, charge)
	{
		//console.log('append', atomOrSubFormula.getClassName(), atomOrSubFormula.getSymbol && atomOrSubFormula.getSymbol(), count, charge);
		var result = this.createSectionItem(atomOrSubFormula, count, charge);
		this.getSections().push(result);
		this.notifySectionsChanged();
		return result;
	},
	/**
	 * Insert a new section to specified position. If index > this.getSectionCount(), the item
	 *   will be appended to the tail.
	 * @param {Int} index Prefered position of new section item.
	 * @param {Variant} atomOrSubFormula Instance of {@link Kekule.AbstractAtom} or {@link Kekule.MolecularFormula}
	 * @param {Float} count Count of added object in formula. Float value (0.5 and so on) are allowed.
	 * @param {Float} charge Charge of this object, can be float to indicate partial charge. Default is 0.
	 * @returns {Object} Section item added.
	 */
	insertSection: function(index, atomOrSubFormula, count, charge)
	{
		var result = this.createSectionItem(atomOrSubFormula, count, charge);
		this.getSections().splice(index, 0, result);
		this.notifySectionsChanged();
		return result;
	},
	/**
	 * Remove a section at index.
	 * @param {Int} index
	 */
	removeSectionAt: function(index)
	{
		var r = this.getSections().splice(index, 1);
		if (r)
			this.notifySectionsChanged();
		return r;
	},
	/**
	 * Clear all sections and empty the whole formula object.
	 */
	clear: function()
	{
		this.beginUpdate();
		try
		{
			this.setCharge(null);
			this.setRadical(null);
			this.setPropStoreFieldValue('sections', []);
			this.notifySectionsChanged();
		}
		finally
		{
			this.endUpdate();
		}
	},
	/**
	 * Remove a section with atomOrSubFormula.
	 * @param {Variant} atomOrSubFormula Instance of {@link Kekule.AbstractAtom} or {@link Kekule.MolecularFormula}
	 */
	removeObj: function(atomOrSubFormula)
	{
		var i = this.indexOfObj(atomOrSubFormula);
		if (i >= 0)
		{
			var r = this.removeSectionAt(i);
			this.notifySectionsChanged();
		}
		else
			return null;
	},

	/**
	 * Returns max nested level of current formula object.
	 * For example, [Cu(NH3)2]2+SO42-, level of [Cu(NH3)2] is 1 and (NH3) is 0.
	 * @returns {Int}
	 */
	getMaxNestedLevel: function()
	{
		var result = 0;
		for (var i = 0, l = this.getSectionCount(); i < l; ++i)
		{
			var section = this.getSectionAt(i);
			if (section.obj instanceof Kekule.MolecularFormula)
			{
				var nestLevel = section.obj.getMaxNestedLevel() + 1;
				if (nestLevel > result)
					result = nestLevel;
			}
		}
		return result;
	},

	/**
	 * Returns a pure array of {isotope, count, charge}.
	 * For example, [Cu(NH3)4]2+ SO4 2-] will be regarded as CuN4H12SO4
	 */
	getSimpleIsotopeMaps: function()
	{
		//TODO: not finished
	}
});


/**
 * A connection table for representing a complex (especially organic) molecule or group.
 * @class
 * @augments ObjectEx
 * @param {Kekule.ChemSpace} owner Owner for each objects in connection table.
 * @param {Kekule.StructureFragment} parent Parent to hold this Ctab.
 *
 * @property {Kekule.ChemSpace} owner Owner for each objects in connection table.
 * @property {Kekule.StructureFragment} parent Parent to hold this Ctab. Read only.
 * @property {Array} nodes All structure nodes in this connection table.
 * @property {Array} anchorNodes Nodes that can have bond connected to other structure fragments.
 * @property {Array} connectors Connectors (usually bonds) in this connection table.
 * @property {Array} nonHydrogenNodes All structure nodes except hydrogen atoms in this connection table.
 * @property {Array} nonHydrogenConnectors Connectors except ones connected to hydrogen atoms in this connection table.
 */
Kekule.StructureConnectionTable = Class.create(ObjectEx,
/** @lends Kekule.StructureConnectionTable# */
{
	/** @private */
	CLASS_NAME: 'Kekule.StructureConnectionTable',
	/** @private */
	TRAVERS_VISITED_KEY: '__$ctabTraversVisited__',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */owner, parent)
	{
		this.tryApplySuper('initialize')  /* $super() */;
		if (owner)
			this.setOwner(owner);
		if (parent)
			this.setPropStoreFieldValue('parent', parent);
		//this.setBubbleEvent(true);  // allow event bubble
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('owner', {
			'dataType': 'Kekule.ChemSpace',
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': function(value)
				{
					if (value != this.getPropStoreFieldValue('owner'))
					{
						this.setPropStoreFieldValue('owner', value);
						this.ownerChanged(value);
					}
				}
		});
		this.defineProp('parent', {
			'dataType': 'Kekule.StructureFragment', 'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': function(value)
				{
					if (value != this.getPropStoreFieldValue('parent'))
					{
						this.setPropStoreFieldValue('parent', value);
						this.parentChanged(value);
					}
				}
		});

		// Usually you are not to set nodes property directly. But some canonicalizers may need to replace the while node field.
		this.defineProp('nodes', {'dataType': DataType.ARRAY});

		this.defineProp('anchorNodes', {'dataType': DataType.ARRAY, 'setter': null});

		// Usually you are not to set connectors property directly. But some canonicalizers may need to replace the while connector field.
		this.defineProp('connectors', {'dataType': DataType.ARRAY});

		this.defineProp('nonHydrogenNodes', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC,
			'serializable': false,
			'setter': null,
			'getter': function()
			{
				var result = [];
				for (var i = 0, l = this.getNodeCount(); i < l; ++i)
				{
					var node = this.getNodeAt(i);
					/*
					if (!node.isHydrogenAtom || !node.isHydrogenAtom())
						result.push(node);
					*/
					if (node instanceof Kekule.ChemStructureNode)
					{
						if (!node.isHydrogenAtom || !node.isHydrogenAtom())
							result.push(node);
					}
				}
				return result;
			}
		});
		this.defineProp('nonHydrogenConnectors', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC,
			'serializable': false,
			'setter': null,
			'getter': function()
			{
				var result = [];
				for (var i = 0, l = this.getConnectorCount(); i < l; ++i)
				{
					var conn = this.getConnectorAt(i);
					if (!conn.isNormalConnectorToHydrogen || !conn.isNormalConnectorToHydrogen())
						result.push(conn);
				}
				return result;
			}
		});

		this.defineProp('structureCache', {'dataType': DataType.HASH, 'serializable': false,
			// 'scope': Class.PropertyScope.PUBLIC,
			'setter': null,   // do not allow change it directly, use method setStructureCacheData instead
			'getter': function(autoCreate)
			{
				var p = this.getParent();
				return p && p.getStructureCache(autoCreate);
			}
		});
	},
	/** @private */
	initPropValues: function(/*$super*/)
	{
		this.tryApplySuper('initPropValues')  /* $super() */;
		this.setPropStoreFieldValue('nodes', []);
		this.setPropStoreFieldValue('anchorNodes', []);
		this.setPropStoreFieldValue('connectors', []);
	},
	/** @private */
	getHigherLevelObj: function()
	{
		return this.getParent() || this.getOwner();
	},

	// custom save / load method
	/** @private */
	doSaveProp: function(obj, prop, storageNode, options, serializer, rootObj, rootStorageNode, handledObjs)
	{
		var propName = prop.name;
		switch (propName)
		{
			case 'anchorNodes':
				{
					var indexes = [];
					for (var i = 0, l = obj.getAnchorNodeCount(); i < l; ++i)
					{
						var node = obj.getAnchorNodeAt(i);
						var index = obj.indexOfNode(node);
						indexes.push(index);
					}
					var subNode = serializer.createChildStorageNode(storageNode, serializer.propNameToStorageName(propName), true); // create sub node for array
					serializer.saveObj(indexes, subNode, options, rootObj, rootStorageNode, handledObjs);
					return true;  // this property is handled, do not use default save method
					break;
				}
			case 'connectors':
				{
					// TODO: now cross connectors (connected node outside this ctab) is not considered
					var subNode = serializer.createChildStorageNode(storageNode, serializer.propNameToStorageName(propName), true); // create sub node for array
					for (var i = 0, l = obj.getConnectorCount(); i < l; ++i)
					{
						var item = obj.getConnectorAt(i);
						var nodeName = serializer.getNameForArrayItemStorageNode(item);
						var itemNode = serializer.appendArrayItemStorageNode(subNode,
							serializer.propNameToStorageName(nodeName), serializer.isArray(item));

						serializer.setStorageNodeExplicitType(itemNode, serializer.getValueExplicitType(item));
						serializer.saveObj(item, itemNode, options, rootObj, rootStorageNode, handledObjs);
						// as connector's connectedObjs property is marked as unserializable, need to be handled here
						/*
						var connectedNodes = [];
						var connectedConnectors = [];
						*/
						var connectedObjs = [];
						for (var j = 0, k = item.getConnectedObjCount(); j < k; ++j)
						{
							var conObj = item.getConnectedObjAt(j);
							/*
							var index = obj.indexOfNode(conObj);
							if (index >= 0)
								connectedNodes.push(index);
							else
							{
								index = obj.indexOfConnector(conObj);
								if (index >= 0)
									connectedConnectors.push(index);
								else  // not in direct children, check nested structures's nodes
								{
									var indexStack = obj.indexStackOfNode(conObj);
									if ((indexStack !== null)&& (typeof(indexStack) !== 'undefined'))
									{
										connectedNodes.push(indexStack);
									}
								}
							}
							*/
							var index = obj.indexOfChild(conObj);
							if (index >= 0)
								connectedObjs.push(index);
							else  // not in direct children, check nested structures's nodes
							{
								var indexStack = obj.indexStackOfChild(conObj);
								if ((indexStack !== null)&& (typeof(indexStack) !== 'undefined'))
								{
									connectedObjs.push(indexStack);
								}
							}
						}
						// save connected array
						/*
						if (connectedNodes.length)
						{
							var conNodeNode = serializer.createChildStorageNode(itemNode, serializer.propNameToStorageName('connectedNodes'), true); // create sub node for array
							serializer.save(connectedNodes, conNodeNode);
						}
						if (connectedConnectors.length)
						{
							var conConnectorNode = serializer.createChildStorageNode(itemNode, serializer.propNameToStorageName('connectedConnectors'), true); // create sub node for array
							serializer.save(connectedConnectors, conConnectorNode);
						}
						*/
						if (connectedObjs.length)
						{
							var conObjsNode = serializer.createChildStorageNode(itemNode, serializer.propNameToStorageName('connectedObjs'), true); // create sub node for array
							serializer.saveObj(connectedObjs, conObjsNode, options, rootObj, rootStorageNode, handledObjs);
						}
					}
					return true;
					break;
				}
			default:
				return false;  // use default save method
		}
	},
	/** @private */
	doLoadProp: function(obj, prop, storageNode, serializer, rootObj, rootStorageNode, handledObjs)
	{
		var propName = prop.name;
		switch (propName)
		{
			case 'anchorNodes':
				{
					var indexes = [];
					var subNode = serializer.getChildStorageNode(storageNode, serializer.propNameToStorageName(propName)); // get sub node for array
					serializer.loadObj(indexes, subNode, rootObj, rootStorageNode, handledObjs);
					obj.__load_anchorNodes_indexes = indexes; // save the indexes and handle it after all properties are loaded
					return true;  // this property is handled, do not use default save method
					break;
				}
			case 'connectors':
			{
				var subNode = serializer.getChildStorageNode(storageNode, serializer.propNameToStorageName(propName)); // get sub node for array
				var itemNodes = serializer.getAllArrayItemStorageNodes(subNode);
				var connector;
				for (var i = 0, l = itemNodes.length; i < l; ++i)
				{
					var itemNode = itemNodes[i];
					var valueType = serializer.getStorageNodeExplicitType(itemNode) || serializer.getStorageNodeName(itemNode);
					if (valueType)
					{
						connector = DataType.createInstance(valueType);
						serializer.loadObj(connector, itemNode, rootObj, rootStorageNode, handledObjs);
						// then handle connectedObjs array
						var conObjsNode = serializer.getChildStorageNode(itemNode, serializer.propNameToStorageName('connectedObjs'));
						// as some object in Ctab may not be loaded, we just mark the indexes and handle it after loading process is done
						if (conObjsNode)
						{
							var connectedObjs = [];
							serializer.loadObj(connectedObjs, conObjsNode, rootObj, rootStorageNode, handledObjs);
							connector.__load_connectedObj_indexes = connectedObjs;
						}

						// conNode/conConnector are for back compatity
						var conNodeNode = serializer.getChildStorageNode(itemNode, serializer.propNameToStorageName('connectedNodes'));
						// as some object in Ctab may not be loaded, we just mark the indexes and handle it after loading process is done
						if (conNodeNode)
						{
							var connectedNodes = [];
							serializer.loadObj(connectedNodes, conNodeNode, rootObj, rootStorageNode, handledObjs);
							connector.__load_connectedNode_indexes = connectedNodes;
						}
						var conConnectorNode = serializer.getChildStorageNode(itemNode, serializer.propNameToStorageName('connectedConnectors'));
						if (conConnectorNode)
						{
							var connectedConnectors = [];
							serializer.loadObj(connectedConnectors, conConnectorNode, rootObj, rootStorageNode, handledObjs);
							connector.__load_connectedConnector_indexes = connectedConnectors;
						}

						obj.appendConnector(connector);
					}
				}
				return true;
				break;
			}
			default:
				return false;  // use default save method
		}
	},
	/** @private */
	loaded: function(rootObj)
	{
		// after loaded, set anchorNodes
		if (this.__load_anchorNodes_indexes)
		{
			for (var i = 0, l = this.__load_anchorNodes_indexes.length; i < l; ++i)
			{
				var index = this.__load_anchorNodes_indexes[i];
				this.appendAnchorNode(this.getNodeAt(index));
			}
			delete this.__load_anchorNodes_indexes;
		}
		// and set connectedObjs of each connectors
		for (var i = 0, l = this.getConnectorCount(); i < l; ++i)
		{
			var connector = this.getConnectorAt(i);
			if (connector.__load_connectedObj_indexes)
			{
				for (var j = 0, k = connector.__load_connectedObj_indexes.length; j < k; ++j)
				{
					var index = connector.__load_connectedObj_indexes[j];
					var connObj;
					if (typeof(index) == 'object')  // is array, actually the index stack
						connObj = this.getChildAtIndexStack(index);
					else  // normal stack
						connObj = this.getChildAt(index);
					if (connObj)
						connector.appendConnectedObj(connObj);
				}
				delete connector.__load_connectedObj_indexes;
			}
			//else // for back compatity
			{
				if (connector.__load_connectedNode_indexes)
				{
					for (var j = 0, k = connector.__load_connectedNode_indexes.length; j < k; ++j)
					{
						var index = connector.__load_connectedNode_indexes[j];
						if (typeof(index) == 'object')  // is array, actually the index stack
							connector.appendConnectedObj(this.getNodeAtIndexStack(index));
						else  // normal stack
							connector.appendConnectedObj(this.getNodeAt(index));
					}
					delete connector.__load_connectedNode_indexes;
				}
				if (connector.__load_connectedConnector_indexes)
				{
					for (var j = 0, k = connector.__load_connectedConnector_indexes.length; j < k; ++j)
					{
						var index = connector.__load_connectedConnector_indexes[j];
						connector.appendConnectedObj(this.getConnectorAt(index));
					}
					delete connector.__load_connectedConnector_indexes;
				}
			}
		}
		this.ownerChanged(this.getOwner());
		this.parentChanged(this.getParent());  // update owner and parent of child objects
		this.tryApplySuper('loaded', [rootObj]);
	},
	/**
	 * Notify {@link Kekule.StructureConnectionTable#nodes} property has been changed
	 * @private
	 */
	notifyNodesChanged: function()
	{
		this.notifyPropSet('nodes', this.getPropStoreFieldValue('nodes'));
	},
	/**
	 * Notify {@link Kekule.StructureConnectionTable#anchorNodes} property has been changed
	 * @private
	 */
	notifyAnchorNodesChanged: function()
	{
		this.notifyPropSet('anchorNodes', this.getPropStoreFieldValue('anchorNodes'));
	},
	/**
	 * Notify {@link Kekule.StructureConnectionTable#connectors} property has been changed
	 * @private
	 */
	notifyConnectorsChanged: function()
	{
		this.notifyPropSet('connectors', this.getPropStoreFieldValue('connectors'));
	},

	/**
	 * Called after a new owner property is set.
	 * Note Connection table is not inherited from ChemObject, so no $super() need to be called.
	 * @private
	 */
	ownerChanged: function(newOwner)
	{
		// change nodes and connectors' owner
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			this.getNodeAt(i).setOwner(newOwner);
		for (var i = 0, l = this.getConnectorCount(); i < l; ++i)
			this.getConnectorAt(i).setOwner(newOwner);
	},
	/**
	 * Called after a new parent property is set.
	 * Note Connection table is not inherited from ChemObject, so no $super() need to be called.
	 * @private
	 */
	parentChanged: function(/*$super, */newParent, oldParent)
	{
		// change nodes and connectors' parent
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			this.getNodeAt(i).setParent(newParent);
		for (var i = 0, l = this.getConnectorCount(); i < l; ++i)
			this.getConnectorAt(i).setParent(newParent);
	},

	/**
	 * Returns if this fragment has no formula or ctab, or ctab has no nodes or connectors.
	 * @return {Bool}
	 */
	isEmpty: function()
	{
		return (this.getNodeCount() <= 0) && (this.getConnectorCount() <= 0);
	},

	/**
	 * Retrieve a cached structure data item.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getStructureCacheData: function(key)
	{
		var cache = this.getStructureCache();
		return cache && cache[key];
	},
	/**
	 * Set a cached structure data item.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setStructureCacheData: function(key, value)
	{
		//this.getStructureCache(true)[key] = value;
		var p = this.getParent();
		if (p && p.setStructureCacheData)
			p.setStructureCacheData(key, value);
		return this;
	},
	/**
	 * Clear all cached structure data.
	 */
	clearStructureCache: function()
	{
		var p = this.getParent();
		if (p)
			p.clearStructureCache();
		return this;
	},

	/**
	 * Get a structure node object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.BaseStructureNode}
	 */
	getNodeById: function(id)
	{
		var nodes = this.getNodes();
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			if (nodes[i].getId() == id)
				return nodes[i];
		}
		return null;
	},
	/**
	 * Get a structure connector object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.BaseStructureConnector}
	 */
	getConnectorById: function(id)
	{
		var connectors = this.getConnectors();
		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			if (connectors[i].getId() == id)
				return connectors[i];
		}
		return null;
	},
	/**
	 * Get a structure node or connector object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.ChemStructureObject}
	 */
	getObjectById: function(id)
	{
		var node = this.getNodeById(id);
		return node? node: this.getConnectorById(id);
	},
	/**
	 * Return count of nodes.
	 * @returns {Int}
	 */
	getNodeCount: function()
	{
		return this.getNodes().length;
	},
	/**
	 * Get node at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemStructureNode}
	 */
	getNodeAt: function(index)
	{
		return this.getNodes()[index];
	},
	/**
	 * Get index of node in nodes list.
	 * @param {Kekule.ChemStructureNode} node
	 * @returns {Int}
	 */
	indexOfNode: function(node)
	{
		return this.getNodes().indexOf(node);
	},
	/**
	 * Check if a node exists in structure.
	 * @param {Kekule.BaseStructureNode} node Node to seek.
	 * @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
	 * @returns {Bool}
	 */
	hasNode: function(node, checkNestedStructure)
	{
		var found = this.indexOfNode(node) >= 0;
		if ((!found) && checkNestedStructure)
		{
			for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			{
				var n = this.getNodeAt(i);
				//if (n instanceof Kekule.StructureFragment)
				if (n.hasNode)
					found = n.hasNode(node, true);
				if (found)
					break;
			}
		}
		return found;
	},
	/**
	 * Returns index of node. If node exists in nested sub group, index in sub group will be pushed to stack as well.
	 * @param {Kekule.BaseStructureNode} node
	 * @returns {Variant} If node is the direct child of this structure, returns {Int}, otherwise stack {Array} will be returned.
	 */
	indexStackOfNode: function(node)
	{
		var index = this.indexOfNode(node);
		if (index >= 0)
			return index;
		else  // check nested structures
		{
			var stack = null;
			for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			{
				var n = this.getNodeAt(i);
				//if (n instanceof Kekule.StructureFragment)
				if (n.indexStackOfNode)
				{
					stack = n.indexStackOfNode(node);
					if (!Kekule.ObjUtils.isUnset(stack))  // found
					{
						// push index of n to stack
						if (typeof(stack) != 'object')
							stack = [stack];
						stack.unshift(i);
						//console.log('get stack: ', stack);
						return stack;
					}
				}
			}
			return stack;
		}
	},
	/**
	 * Get node at indexStack.
	 * For example, indexStack is [2, 3, 1], then this.getNodeAt(2).getNodeAt(3).getNodeAt(1) will be returned.
	 * @param {Array} indexStack Array of integers.
	 * @returns {Kekule.BaseStructureNode}
	 */
	getNodeAtIndexStack: function(indexStack)
	{
		indexStack = Kekule.ArrayUtils.toArray(indexStack);
		if (indexStack.length <= 0)
			return null;
		else
		{
			var node = this.getNodeAt(indexStack[0]);
			for (var i = 1, l = indexStack.length; i < l; ++i)
			{
				if (node && node.getNodeAt)
					node = node.getNodeAt(indexStack[i]);
				else
					return null;
			}
			return node;
		}
	},
	/**
	 * Insert a node before refNode.
	 * @param {Kekule.SimpleStructureNode} node
	 * @param {Kekule.SimpleStructureNode} refNode
	 */
	insertNodeBefore: function(node, refNode)
	{
		var refIndex = this.indexOfNode(refNode);
		if (refIndex)
			this.insertNodeAt(node, refIndex);
		else
			this.appendNode(node);
	},
	/**
	 * Add node to connection table. If node already inside, nothing will be done.
	 * @param {Kekule.SimpleStructureNode} node
	 */
	appendNode: function(node)
	{
		var r = Kekule.ArrayUtils.pushUniqueEx(this.getNodes(), node);
		if (r.isPushed)
		{
			if (node.setOwner)
				node.setOwner(this.getOwner());
			if (node.setParent)
				node.setParent(this.getParent());
			this.notifyNodesChanged();
		}
		return r.index;
	},
	/**
	 * Insert node to index. If index is not set, node will be inserted to the tail of node list of ctab.
	 * @param {Kekule.BaseStructureNode} node
	 * @param {Int} index
	 */
	insertNodeAt: function(node, index)
	{
		var i = this.indexOfNode(node);
		var nodes = this.getNodes();
		if (Kekule.ObjUtils.isUnset(index) || (index < 0))
			index = nodes.length;
		if (i >= 0)  // already inside, adjust position
		{
			nodes.splice(i, 1);
			nodes.splice(index, 0, node);
		}
		else // new one
		{
			nodes.splice(index, 0, node);
			if (node.setOwner)
				node.setOwner(this.getOwner());
			if (node.setParent)
				node.setParent(this.getParent());
		}
		this.notifyNodesChanged();
		return index;
	},
	/**
	 * Change index of node.
	 * @param {Kekule.BaseStructureNode} node
	 * @param {Int} index
	 */
	setNodeIndex: function(node, index)
	{
		var nodes = this.getNodes();
		var i = this.indexOfNode(node);
		if (i >= 0)  // already inside, adjust position
		{
			nodes.splice(i, 1);
			nodes.splice(index, 0, node);
		}
	},
	/**
	 * Remove node at index in connection table.
	 * @param {Int} index
	 * @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
	 */
	removeNodeAt: function(index, preserveLinkedConnectors)
	{
		var node = this.getNodes()[index];
		if (node)
		{
			// remove from connectors
			if (!preserveLinkedConnectors)
				node.removeThisFromLinkedConnector();
			//this.removeConnectNode(node);
			//this.getNodes().removeAt(index);
			this.getNodes().splice(index, 1);
			if (node.setOwner)
				node.setOwner(null);
			if (node.setParent)
				node.setParent(null);
			this.notifyNodesChanged();
		}
	},
	/**
	 * Remove a node in connection table.
	 * @param {Kekule.BaseStructureNode} node
	 * @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
	 */
	removeNode: function(node, preserveLinkedConnectors)
	{
		var index = this.getNodes().indexOf(node);
		if (index >= 0)
			this.removeNodeAt(index, preserveLinkedConnectors);
	},
	/**
	 * Replace oldNode with new one, preserve coords and all linked connectors.
	 * @param {Kekule.BaseStructureNode} oldNode Must be direct child of current ctab (node in nested structure fragment will be ignored).
	 * @param {Kekule.BaseStructureNode} newNode
	 */
	replaceNode: function(oldNode, newNode)
	{
		if (!this.hasNode(oldNode))
		{
			return this;
		}
		else
		{
			//console.log('replace', oldNode.getClassName(), newNode.getClassName());
			this.insertBefore(newNode, oldNode);
			// replace in anchor nodes
			var anchorNodes = this.getAnchorNodes();
			var anchorIndex = anchorNodes.indexOf(oldNode);
			if (anchorIndex >= 0)
				anchorNodes.splice(anchorIndex, 1, [newNode]);
			// replace connectors
			var connectors = Kekule.ArrayUtils.clone(oldNode.getLinkedConnectors());  // clone array, otherwise linked connectors will change
			for (var i = 0, l = connectors.length; i < l; ++i)
			{
				var connector = connectors[i];
				connector.replaceConnectedObj(oldNode, newNode);
			}

			// replace related cross connectors of oldNode (when it is subgroup)
			if (oldNode.getNodes && oldNode.getCrossConnectors)
			{
				var crossConnectors = Kekule.ArrayUtils.clone(oldNode.getCrossConnectors());
				for (var i = 0, l = crossConnectors.length; i < l; ++i)
				{
					var connector = crossConnectors[i];
					if (this.indexOfConnector(connector) >= 0)  // is connector in this ctab
					{
						for (var j = 0, k = connector.getConnectedObjCount(); j < k; ++j)
						{
							var obj = connector.getConnectedObjAt(j);
							if (oldNode.indexOfChild(obj) >= 0)  // obj is inside oldNode group, replace it with new Node
							  connector.replaceConnectedObj(obj, newNode);
						}
					}
					connector.replaceConnectedObj(oldNode, newNode);
				}
			}

			this.removeNode(oldNode);
		}
	},
	/**
	 * Remove all nodes from connection table.
	 */
	clearNodes: function()
	{
		this.clearAnchorNodes();
		this.setPropStoreFieldValue('nodes', []);
		this.notifyAnchorNodesChanged();
		this.notifyNodesChanged();
	},

	/**
	 * Sort direct child nodes in ctab.
	 * @param {Function} sortFunc Function to determine the priority of nodes.
	 */
	sortNodes: function(sortFunc)
	{
		if (sortFunc)
		{
			this.getNodes().sort(sortFunc);
			this.notifyNodesChanged();
		}
		else
			Kekule.error(/*Kekule.ErrorMsg.SORT_FUNC_UNSET*/Kekule.$L('ErrorMsg.SORT_FUNC_UNSET'));
	},

	/**
	 * Check if child nodes has 2D coord.
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoord2D: function(allowCoordBorrow, checkZeroCoord)
	{
		/*
		var nodes = this.getNodes();
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			if (nodes[i].hasCoord2D(allowCoordBorrow))
				return true;
		}
		return false;
		*/
		return this.nodesHasCoordOfMode(Kekule.CoordMode.COORD2D, allowCoordBorrow, checkZeroCoord);
	},
	/**
	 * Check if child nodes has 3D coord.
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoord3D: function(allowCoordBorrow, checkZeroCoord)
	{
		/*
		var nodes = this.getNodes();
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			if (nodes[i].hasCoord3D(allowCoordBorrow))
				return true;
		}
		return false;
		*/
		return this.nodesHasCoordOfMode(Kekule.CoordMode.COORD3D, allowCoordBorrow, checkZeroCoord);
	},
	/**
	 * Check if child nodes has coord on coordMode.
	 * @param {Int} coordMode
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoordOfMode: function(coordMode, allowCoordBorrow, checkZeroCoord)
	{
		var isZero = Kekule.CoordUtils.isZero;
		var zeroCount = 0;
		var nodes = this.getNodes();
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			if (nodes[i].hasCoordOfMode(coordMode, allowCoordBorrow))
			{
				if (checkZeroCoord)
				{
					var coord = nodes[i].getCoordOfMode(coordMode, allowCoordBorrow);
					if (isZero(coord))
						++zeroCount;
					else
						return true;
				}
				else
					return true;
			}
		}
		if (checkZeroCoord && zeroCount === nodes.length && zeroCount <= 1)  // only one node with zero coord, still returns true
			return true;
		else
			return false;
	},

	/**
	 * Create a new Kekule.Atom instance.
	 * @param {Variant} elemSymbolOrAtomicNumber
	 * @param {Number} massNumber
	 * @param {Hash} coord2D
	 * @param {Hash} coord3D
	 * @returns {Kekule.Atom}
	 * @private
	 */
	_createAtom: function(elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D)
	{
		var atom = new Kekule.Atom(null, elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D);
		return atom;
	},

	/**
	 * Util method to create a new Kekule.Atom instance and append to current connection table.
	 * @param {Variant} elemSymbolOrAtomicNumber
	 * @param {Number} massNumber
	 * @param {Hash} coord2D
	 * @param {Hash} coord3D
	 * @returns {Kekule.Atom}
	 * @private
	 */
	appendAtom: function(elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D)
	{
		var atom = this._createAtom(elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D);
		if (atom)
			this.appendNode(atom);
		return atom;
	},

	/**
	 * Return count of anchorNodes.
	 * @returns {Int}
	 */
	getAnchorNodeCount: function()
	{
		return this.getAnchorNodes().length;
	},
	/**
	 * Get index of node in anchorNodes list.
	 * @param {Kekule.ChemStructureNode} node
	 * @returns {Int}
	 */
	indexOfAnchorNode: function(node)
	{
		return this.getAnchorNodes().indexOf(node);
	},
	/**
	 * Get anchor node at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemStructureNode}
	 */
	getAnchorNodeAt: function(index)
	{
		return this.getAnchorNodes()[index];
	},
	/**
	 * Add anchor node of connection table. If node not in nodes, nothing will be done.
	 * @param {Kekule.ChemStructureNode} node
	 */
	appendAnchorNode: function(node)
	{
		if (!node)
			return -1;
		if (this.getNodes().indexOf(node) >= 0)
		{
			var r = Kekule.ArrayUtils.pushUniqueEx(this.getAnchorNodes(), node);
			if (r.isPushed)
				this.notifyAnchorNodesChanged();
			return r.index;
		}
		else
			return -1;
	},
	/**
	 * Remove node at index of anchorNodes.
	 * @param {Int} index
	 */
	removeAnchorNodeAt: function(index)
	{
		var node = this.getAnchorNodes()[index];
		if (node)
		{
			//this.getAnchorNodes().removeAt(index);
			this.getAnchorNodes().splice(index, 1);
			this.notifyAnchorNodesChanged();
		}
	},
	/**
	 * Remove a node in anchorNodes.
	 * @param {Kekule.BaseStructureNode} node
	 */
	removeAnchorNode: function(node)
	{
		var index = this.getAnchorNodes().indexOf(node);
		if (index >= 0)
			this.removeAnchorNodeAt(index);
	},
	/**
	 * Remove all anchor nodes from connection table.
	 */
	clearAnchorNodes: function()
	{
		this.setPropStoreFieldValue('anchorNodes', []);
		this.notifyAnchorNodesChanged();
	},
	/**
	 * Return count of connectors.
	 * @returns {Int}
	 */
	getConnectorCount: function()
	{
		return this.getConnectors().length;
	},
	/**
	 * Get index of connector in connectors list.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @returns {Int}
	 */
	indexOfConnector: function(connector)
	{
		return this.getConnectors().indexOf(connector);
	},
	/**
	 * Get connector at index.
	 * @param {Int} index
	 * @returns {Kekule.BaseStructureConnector}
	 */
	getConnectorAt: function(index)
	{
		return this.getConnectors()[index];
	},

	/**
	 * Returns index of connector. If connector exists in nested sub group, index in sub group will be pushed to stack as well.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @returns {Variant} If connector is the direct child of this structure, returns {Int}, otherwise stack {Array} will be returned.
	 */
	indexStackOfConnector: function(connector)
	{
		var index = this.indexOfConnector(connector);
		if (index >= 0)
			return index;
		else  // check nested structures
		{
			var stack = null;
			for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			{
				var n = this.getNodeAt(i);
				if (n.indexStackOfConnector)
				{
					stack = n.indexStackOfConnector(connector);
					if (!Kekule.ObjUtils.isUnset(stack))  // found
					{
						// push index of n to stack
						if (typeof(stack) != 'object')
							stack = [stack];
						stack.unshift(i);
						//console.log('get stack: ', stack);
						return stack;
					}
				}
			}
			return stack;
		}
	},
	/**
	 * Get connector at indexStack.
	 * For example, indexStack is [2, 3, 1], then this.getNodeAt(2).getNodeAt(3).getConnectorAt(1) will be returned.
	 * @param {Array} indexStack Array of integers.
	 * @returns {Kekule.BaseStructureConnector}
	 */
	getConnectorAtIndexStack: function(indexStack)
	{
		indexStack = Kekule.ArrayUtils.toArray(indexStack);
		if (indexStack.length <= 0)
			return null;
		else if (indexStack.length === 1)
		{
			return this.getConnectorAt(indexStack[0]);
		}
		else
		{
			var stackTail = indexStack.length - 1;
			var node = this.getNodeAt(indexStack[0]);
			for (var i = 1; i < stackTail; ++i)
			{
				if (node && node.getNodeAt)
					node = node.getNodeAt(indexStack[i]);
				else
					return null;
			}
			if (node)
				return node.getConnectorAt(indexStack[stackTail]);
			else
				return null;
		}
	},

	/**
	 * Insert a connector before refConnector.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Kekule.BaseStructureConnector} refConnector
	 */
	insertConnectorBefore: function(connector, refConnector)
	{
		var refIndex = this.indexOfConnector(refConnector);
		if (refIndex)
			this.insertConnectorAt(connector, refIndex);
		else
			this.appendConnector(connector);
	},
	/**
	 * Add connector to connection table.
	 * @param {Kekule.BaseStructureConnector} connector
	 */
	appendConnector: function(connector)
	{
		var r = Kekule.ArrayUtils.pushUniqueEx(this.getConnectors(), connector);
		if (r.isPushed)
		{
			if (connector.setOwner)
				connector.setOwner(this.getOwner());
			if (connector.setParent)
				connector.setParent(this.getParent());
			this.notifyConnectorsChanged();
		}
		return r.index;
	},
	/**
	 * Insert connector to index. If index is not set, node will be inserted as the first connector of ctab.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Int} index
	 */
	insertConnectorAt: function(connector, index)
	{
		var i = this.indexOfConnector(connector);
		var connectors = this.getConnectors();
		if (Kekule.ObjUtils.isUnset(index) || (index < 0))
			index = connectors.length;
		if (i >= 0)  // already inside, adjust position
		{
			connectors.splice(i, 1);
			connectors.splice(index, 0, connector);
		}
		else // new one
		{
			connectors.splice(index, 0, connector);
			if (connector.setOwner)
				connector.setOwner(this.getOwner());
			if (connector.setParent)
				connector.setParent(this.getParent());
		}
		this.notifyConnectorsChanged();
	},
	/**
	 * Change index of connector.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Int} index
	 */
	setConnectorIndex: function(connector, index)
	{
		var connectors = this.getConnectors();
		var i = this.indexOfConnector(connector);
		if (i >= 0)  // already inside, adjust position
		{
			connectors.splice(i, 1);
			connectors.splice(index, 0, connector);
		}
	},
	/**
	 * Remove connector at index of connectors.
	 * @param {Int} index
	 * @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
	 */
	removeConnectorAt: function(index, preserveConnectedObjs)
	{
		var connector = this.getConnectors()[index];
		if (connector)
		{
			//this.getConnectors().removeAt(index);
			if (!preserveConnectedObjs)
				connector.removeThisFromConnectedObjs();
			this.getConnectors().splice(index, 1);
			if (connector.setOwner)
				connector.setOwner(null);
			if (connector.setParent)
				connector.setParent(null);
			this.notifyConnectorsChanged();
		}
	},
	/**
	 * Remove a connector in connection table.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
	 */
	removeConnector: function(connector, preserveConnectedObjs)
	{
		var index = this.getConnectors().indexOf(connector);
		if (index >= 0)
			this.removeConnectorAt(index, preserveConnectedObjs);
	},
	/**
	 * Remove all connectors from connection table.
	 */
	clearConnectors: function()
	{
		this.setPropStoreFieldValue('connectors', []);
		this.notifyConnectorsChanged();
	},
	/**
	 * Check if a connector exists in structure.
	 * @param {Kekule.BaseStructureConnector} connector Connector to seek.
	 * @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
	 * @returns {Bool}
	 */
	hasConnector: function(connector, checkNestedStructure)
	{
		var found = this.indexOfConnector(connector) >= 0;
		if ((!found) && checkNestedStructure)
		{
			for (var i = 0, l = this.getNodeCount(); i < l; ++i)
			{
				var n = this.getNodeAt(i);
				//if (n instanceof Kekule.StructureFragment)
				if (n.hasConnector)
					found = n.hasConnector(connector, true);
				if (found)
					break;
			}
		}
		return found;
	},

	/**
	 * Sort direct child connectors in ctab.
	 * @param {Function} sortFunc Function to determine the priority of connectors.
	 */
	sortConnectors: function(sortFunc)
	{
		if (sortFunc)
		{
			this.getConnectors().sort(sortFunc);
			this.notifyConnectorsChanged();
		}
		else
			Kekule.error(/*Kekule.ErrorMsg.SORT_FUNC_UNSET*/Kekule.$L('ErrorMsg.SORT_FUNC_UNSET'));
	},

	/**
	 * A util method to create a new bond object connected with node only.
	 * @param {Array} nodesOrIndexes Array of connected nodes or indexes of those nodes
	 * @param {Int} bondOrder Order of bond.
	 * @param {Int} bondType Type of bond.
	 * @returns {Kekule.Bond}
	 * @private
	 */
	_createBond: function(nodesOrIndexes, bondOrder, bondType)
	{
		var connectedObjs = [];
		for (var i = 0, l = nodesOrIndexes.length; i < l; ++i)
		{
			var a = nodesOrIndexes[i];
			if (DataType.isSimpleValue(a))  // not node object, should be index of node
				a = this.getNodeAt(a);
			connectedObjs.push(a);
		}
		var result = new Kekule.Bond(null, connectedObjs, bondOrder, null, bondType);
		return result;
	},
	/**
	 * A util method to create a new bond object connected with nodes and append to current connection table.
	 * @param {Array} nodesOrIndexes Array of connected nodes or indexes of those nodes
	 * @param {Int} bondOrder Order of bond.
	 * @param {Int} bondType Type of bond.
	 * @returns {Kekule.Bond}
	 */
	appendBond: function(nodesOrIndexes, bondOrder, bondType)
	{
		var bond = this._createBond(nodesOrIndexes, bondOrder, bondType);
		if (bond)
			this.appendConnector(bond);
		return bond;
	},

	/**
	 * Returns nodes or connectors that should be removed cascadely with chemStructObj.
	 * @param {Object} childObj
	 * @returns {Array}
	 * @private
	 */
	_getObjsNeedToBeCascadeRemoved: function(childObj, ignoredChildObjs)
	{
		var result = [];
		// all usual connectors (two ends) connected to chemStructObj should be removed
		var linkedConnectors = childObj.getLinkedConnectors? childObj.getLinkedConnectors(): [];
		for (var i = 0, l = linkedConnectors.length; i < l; ++i)
		{
			var connector = linkedConnectors[i];
			if ((!ignoredChildObjs) || (ignoredChildObjs.indexOf(connector) < 0))
			{
				if (connector.getConnectedObjs().length <= 2)
					Kekule.ArrayUtils.pushUnique(result, connector);
			}
		}

		if (childObj instanceof Kekule.ChemStructureNode)
		{
			// no additional objects should be delete
		}
		else if (childObj instanceof Kekule.ChemStructureConnector)
		{
			// nodes connected with and only with this connector should be removed
			var objs = childObj.getConnectedObjs();
			for (var i = 0, l = objs.length; i < l; ++i)
			{
				var obj = objs[i];
				if ((!ignoredChildObjs) || (ignoredChildObjs.indexOf(obj) < 0))
				{
					if (obj instanceof Kekule.ChemStructureNode)
					{
						if (obj.getLinkedConnectors().length <= 1)
							Kekule.ArrayUtils.pushUnique(result, obj);
					}
				}
			}
		}
		else  // other objects
			;

		// then iterate each objects in result, check second level cascade remove objects
		var ignoredObjs = [].concat(result);
		ignoredObjs.push(childObj);
		var secondLevelObjs = [];
		for (var i = 0, l = result.length; i < l; ++i)
		{
			var obj = result[i];
			if (obj !== childObj)
				var cascadeObjs = this._getObjsNeedToBeCascadeRemoved(obj, ignoredObjs);
			Kekule.ArrayUtils.pushUnique(secondLevelObjs, cascadeObjs);
		}
		Kekule.ArrayUtils.pushUnique(result, secondLevelObjs);

		return result;
	},

	/**
	 * Remove childObj from connection table.
	 * @param {Variant} childObj A child node or connector.
	 * @param {Bool} cascadeRemove Whether remove related objects (e.g., bond connected to an atom).
	 * @param {Bool} freeRemoved Whether free all removed objects.
	 */
	removeChildObj: function(childObj, cascadeRemove, freeRemoved)
	{
		var objs;
		if (cascadeRemove)
		{
			objs = this._getObjsNeedToBeCascadeRemoved(childObj);
			Kekule.ArrayUtils.pushUnique(objs, childObj);
		}
		else
		{
			objs = [childObj];
		}
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (obj instanceof Kekule.BaseStructureConnector)
				this.removeConnector(obj);
			else if (obj instanceof Kekule.BaseStructureNode)
				this.removeNode(obj);
		}
		if (freeRemoved)
		{
			for (var i = objs.length - 1; i >= 0; --i)
			{
				var obj = objs[i];
				if (obj.finalize)
					obj.finalize();
			}
		}
	},
	/**
	 * Remove child obj directly from connection table.
	 * @param {Variant} childObj A child node or connector.
	 */
	removeChild: function(/*$super, */obj)
	{
		return this.removeChildObj(obj) || this.tryApplySuper('removeChild', [obj])  /* $super(obj) */;
	},

	/**
	 * Remove and free childObj from connection table.
	 * @param {Variant} childObj A child node or connector.
	 * @param {Bool} cascadeDelete Whether delete related objects (e.g., bond connected to an atom).
	 */
	deleteChildObj: function(childObj, cascadeDelete)
	{
		return this.removeChildObj(childObj, cascadeDelete, true);
	},

	/**
	 * Check if childObj is a child node or connector of this ctab.
	 * @param {Kekule.ChemObject} childObj
	 * @returns {Bool}
	 */
	hasChildObj: function(childObj)
	{
		return this.hasNode(childObj) || this.hasConnector(childObj);
	},

	/**
	 * Returns next sibling node or connector to childObj.
	 * @param {Variant} childObj Node or connector.
	 * @returns {Variant}
	 */
	getNextSiblingOfChild: function(childObj)
	{
		if (childObj instanceof Kekule.SimpleStructureNode)
		{
			var index = this.indexOfNode(childObj);
			if (index >= 0)
				return this.getNodeAt(index + 1);
		}
		else if (childObj instanceof Kekule.BaseStructureConnector)
		{
			var index = this.indexOfConnector(childObj);
			if (index >= 0)
				return this.getConnectorAt(index + 1);
		}
		return null;
	},

	/**
	 * Insert obj before refChild in node or connector list. If refChild is null or does not exists, obj will be append to tail of list.
	 * @param {Variant} obj A node or connector.
	 * @param {Variant} refChild Ref node or connector
	 * @return {Int} Index of obj after inserting.
	 */
	insertBefore: function(obj, refChild)
	{
		if (obj instanceof Kekule.SimpleStructureNode)
		{
			var refIndex = this.indexOfNode(refChild);
			return this.insertNodeAt(obj, refIndex);
		}
		else if (obj instanceof Kekule.BaseStructureConnector)
		{
			var refIndex = this.indexOfConnector(refChild);
			return this.insertConnectorAt(obj, refIndex);
		}
		else
			return -1;
	},

	/**
	 * Clear all nodes, anchor nodes and connectors in connection table.
	 */
	clear: function()
	{
		this.clearNodes();
		this.clearConnectors();
	},

	/**
	 * Get count of child objects (including both nodes and connectors).
	 * @returns {Int}
	 */
	getChildCount: function()
	{
		return this.getNodeCount() + this.getConnectorCount();
	},
	/**
	 * Get child object (including both nodes and connectors) at index.
	 * @param {Int} index
	 * @returns {Variant}
	 */
	getChildAt: function(index)
	{
		var nodeCount = this.getNodeCount();
		if (index < nodeCount)
			return this.getNodeAt(index);
		else
		{
			var connectorCount = this.getConnectorCount();
			if (index < nodeCount + connectorCount)
				return this.getConnectorAt(index - nodeCount);
			else
				return null;
		}
	},
	/**
	 * Get the index of obj in children list.
	 * @param {Variant} obj
	 * @returns {Int} Index of obj or -1 when not found.
	 */
	indexOfChild: function(obj)
	{
		var result;
		if (obj instanceof Kekule.BaseStructureNode)
			result = this.indexOfNode(obj);
		else if (obj instanceof Kekule.BaseStructureConnector)
		{
			result = this.indexOfConnector(obj);
			if (result >= 0)
			{
				var nodeCount = this.getNodeCount();
				result += nodeCount;
			}
		}
		else
		{
			result = -1;
		}
		return result;
	},

	/**
	 * Get child object at indexStack.
	 * For example, indexStack is [2, 3, 1], then this.getNodeAt(2).getNodeAt(3).getChildAt(1) will be returned.
	 * @param {Array} indexStack Array of integers.
	 * @returns {Kekule.ChemStructureObject}
	 */
	indexStackOfChild: function(obj)
	{
		var result;
		if (obj instanceof Kekule.BaseStructureNode)
			result = this.indexStackOfNode(obj);
		else if (obj instanceof Kekule.BaseStructureConnector)
		{
			var nodeCount = this.getNodeCount();
			result = this.indexStackOfConnector(obj);
			if (result.length)  // is array
			{
				result[result.length - 1] += nodeCount;
			}
			else  // simple int value
			{
				result += nodeCount;
			}
		}
		else
			result = -1;
		return result;
	},
	/**
	 * Get child object at indexStack.
	 * For example, indexStack is [2, 3, 1], then this.getChildAt(2).getChildAt(3).getChildAt(1) will be returned.
	 * @param {Array} indexStack Array of integers.
	 * @returns {Kekule.ChemStructureObject}
	 */
	getChildAtIndexStack: function(indexStack)
	{
		indexStack = Kekule.ArrayUtils.toArray(indexStack);
		if (indexStack.length <= 0)
			return null;
		else
		{
			var child = this.getChildAt(indexStack[0]);
			for (var i = 1, l = indexStack.length; i < l; ++i)
			{
				if (child && child.getChildAt)
					child = child.getChildAt(indexStack[i]);
				else
					return null;
			}
			return child;
		}
	},

	/**
	 * Check if a node has a sub structure (has child nodes).
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @param {Kekule.BaseStructureNode} node
	 * @returns {Bool}
	 */
	isSubFragment: function(node)
	{
		return (node.getNodeCount && (node.getNodeCount() > 0));
	},
	/**
	 * Get all sub fragments (node that have children, usually SubGroup).
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @returns {Array} Array of {@link Kekule.StructureFragment}.
	 */
	getSubFragments: function()
	{
		var result = [];
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
		{
			var node = this.getNodeAt(i);
			if (this.isSubFragment(node))
				result.push(node);
		}
		return result;
	},
	/**
	 * Returns whether there are sub fragment(s) (node that have children, usually SubGroup) in this ctab.
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @returns {Bool}
	 */
	hasSubFragments: function()
	{
		var subs = this.getSubFragments();
		return subs && subs.length;
	},
	/**
	 * Get all leaf nodes (node that do not have children, usually atom).
	 * Note if a sub group has no children, it will be regarded as leaf node too.
	 * @returns {Array} Array of {@link Kekule.BaseStructureNode}.
	 */
	getLeafNodes: function()
	{
		var result = [];
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
		{
			var node = this.getNodeAt(i);
			if (!this.isSubFragment(node))
				result.push(node);
			else if (node.getLeafNodes)
			{
				var leafs = node.getLeafNodes();
				result = result.concat(leafs);
			}
		}
		return result;
	},
	/**
	 * Returns the direct node/substructure that contains originObj.
	 * originObj may be a child node or connector of substructure in this ctab.
	 * If originObj is actually not in this ctab, null will be returned.
	 * @param {Kekule.ChemStructureObject} originObj
	 * @returns {Kekule.BaseStructureNode}
	 */
	findDirectChildOfObj: function(originObj)
	{
		var obj = originObj;
		while (obj && this.indexOfChild(obj) < 0)
		{
			obj = obj.getParent? obj.getParent(): null;
		}
		return obj;
	},
	/**
	 * Return all bonds in structure as well as in sub structure.
	 * @returns {Array} Array of {Kekule.BaseStructureConnector}.
	 */
	getAllChildConnectors: function()
	{
		var result = [].concat(this.getConnectors());
		var subFrags = this.getSubFragments();
		for (var i = 0, l = subFrags.length; i < l; ++i)
		{
			if (subFrags[i].getAllChildConnectors)
				result = result.concat(subFrags[i].getAllChildConnectors());
		}
		return result;
	},
	/**
	 * Return all bonds in structure as well as in sub structure.
	 * @returns {Array} Array of {Kekule.BaseStructureConnector}.
	 */
	getAllContainingConnectors: function()
	{
		return this.getAllChildConnectors();
	},

	/**
	 * Returns an array of all isotope, count and charge map, used for calculation of formula.
	 * @private
	 */
	getIsotopeMaps: function()
	{
		var nodes = this.getNodes();
		if (nodes.length <= 0)
			return [];
		else
		{
			/** @private */
			var addIsotope = function(maps, isotope, count, charge)
				{
					var isOldIsotope = false;
					for (var j = 0, k = maps.length; j < k; ++j)
					{
						if (isotope.isSame(maps[j].isotope))
						{
							isOldIsotope = true;
							maps[j].count += count;
							maps[j].charge += charge || 0;
						}
					}
					if (!isOldIsotope)  // new one, add an item
					{
						var item = {'isotope': isotope, 'count': count, 'charge': node.getCharge() || 0};
						maps.push(item);
					}
					return maps;
				};

			var result = [];
			for (var i = 0, l = nodes.length; i < l; ++i)
			{
				var node = nodes[i];
				if (node instanceof Kekule.StructureFragment)
				{
					var subMaps = node.getIsotopeMaps();
					for (var i = 0, l = subMaps.length; i < l; ++i)  // merge with current maps
					{
						result = addIsotope(result, subMaps[i].isotope, subMaps[i].count, subMaps[i].charge || 0);
					}
				}
				else if (node instanceof Kekule.Atom)
				{
					var isotope = node.getIsotope();
					result = addIsotope(result, node.getIsotope(), 1, node.getCharge() || 0);
					// add H connected to node
					var hcount = node.getHydrogenCount();  // || node.getImplicitHydrogenCount();
					if (hcount > 0)
					{
						var helem = Kekule.IsotopeFactory.getIsotope(1);
						result = addIsotope(result, helem, hcount, 0);
					}
				}
			}
		}
		return result;
	},

	/**
	 * Calculate molecular formula from this connection table.
	 * @returns {Kekule.MolecularFormula}
	 */
	calcFormula: function()
	{
		var isotopeMaps = this.getIsotopeMaps();
		if (isotopeMaps.length <= 0)
			return null;

		var result = new Kekule.MolecularFormula();
		for (var i = 0, l = isotopeMaps.length; i < l; ++i)
		{
			// create fake atom
			var fakeAtom = new Kekule.Atom();
			fakeAtom.setIsotope(isotopeMaps[i].isotope);
			//result.appendSection(isotopeMaps[i].isotope, isotopeMaps[i].count, isotopeMaps[i].charge);
			result.appendSection(fakeAtom, isotopeMaps[i].count, isotopeMaps[i].charge);
		}
		return result;
	},

	/**
	 * Calculate the absolute box to fit all nodes.
	 * @param {Array} nodes
	 * @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getNodesContainBox: function(nodes, coordMode, allowCoordBorrow)
	{
		//console.log('begin');
		var is3D = (coordMode === Kekule.CoordMode.COORD3D);
		var result = {};
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			var node = nodes[i];
			//var coord = is3D? node.getCoord3D(): node.getCoord2D();
			//var coord = is3D? node.getAbsCoord3D(allowCoordBorrow): node.getAbsCoord2D(allowCoordBorrow);
			var coord = node.getAbsCoordOfMode(coordMode, allowCoordBorrow);

			if (i == 0)
			{
				result.x1 = result.x2 = coord.x;
				result.y1 = result.y2 = coord.y;
				if (is3D)
					result.z1 = result.z2 = coord.z;
			}
			else
			{
				(coord.x < result.x1)?
					result.x1 = coord.x: (
						(coord.x > result.x2)? result.x2 = coord.x: null);
				(coord.y < result.y1)?
					result.y1 = coord.y: (
						(coord.y > result.y2)? result.y2 = coord.y: null);
				if (is3D)
					(coord.z < result.z1)?
						result.z1 = coord.z: (
							(coord.z > result.z2)? result.z2 = coord.z: null);
			}
		}
		//console.log('end', result);
		return result;
	},

	/**
	 * Calculate the box to fit all nodes in CTable of molecule.
	 * @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox: function(coordMode, allowCoordBorrow)
	{
		/*
		var is3D = (coordMode === Kekule.CoordMode.COORD3D);
		var result = {};
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
		{
			var node = this.getNodeAt(i);
			var coord = is3D? node.getCoord3D(): node.getCoord2D();
			if (i == 0)
			{
				result.x1 = result.x2 = coord.x;
				result.y1 = result.y2 = coord.y;
				if (is3D)
					result.z1 = result.z2 = coord.z;
			}
			else
			{
				(coord.x < result.x1)?
					result.x1 = coord.x: (
						(coord.x > result.x2)? result.x2 = coord.x: null);
				(coord.y < result.y1)?
					result.y1 = coord.y: (
						(coord.y > result.y2)? result.y2 = coord.y: null);
				if (is3D)
					(coord.z < result.z1)?
						result.z1 = coord.z: (
							(coord.z > result.z2)? result.z2 = coord.z: null);
			}
		}
		return result;
		*/
		return this.getNodesContainBox(this.getNodes(), coordMode, allowCoordBorrow);
	},
	/**
	 * Calculate the 2D box to fit all nodes in CTable.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} 2D box information. {x1, y1, x2, y2, width, height}.
	 */
	getContainerBox2D: function(allowCoordBorrow)
	{
		var result = this.getContainerBox(Kekule.CoordMode.COORD2D, allowCoordBorrow);
		result.width = result.x2 - result.x1;
		result.height = result.y2 - result.y1;
		return result;
	},
	/**
	 * Calculate the 3D box to fit all nodes in CTable.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} 3D box information. {x1, y1, z1, x2, y2, z2, deltaX, deltaY, deltaZ}.
	 */
	getContainerBox3D: function(allowCoordBorrow)
	{
		var result = this.getContainerBox(Kekule.CoordMode.COORD3D, allowCoordBorrow);
		result.deltaX = result.x2 - result.x1;
		result.deltaY = result.y2 - result.y1;
		result.deltaZ = result.z2 - result.z1;
		return result;
	},

	/**
	 * Traverse the nodes in connection tab through a depth or breadth first spanning tree algorithm.
	 * @param {Func} callback Function called when meet a new node or connector, has two params: callback(currNodeOrConnector, isConnector)
	 * @param {Kekule.BaseStructureNode} startingNode Starting position of travers.
	 * @param {Bool} breadthFirst Set to true to use breadth first algorithm or false to use depth first algorithm.
	 * @param {Array} partialNodes If this param is set, only part of the structure will be traversed.
	 * @returns {Hash} A hash object containing all the nodes and connectors sequence traversed. {nodes, connectors}.
	 *   Note that all nodes but not all connectors (e.g., the one in ring) may be traversed.
	 */
	traverse: function(callback, startingNode, breadthFirst, partialNodes)
	{
		var result = {'nodes': [], 'connectors': []};
		var VISITED_KEY = '__$ctabTraverseVisited__';

		var remainingNodes = AU.clone(partialNodes || this.getNodes());
		// init
		for (var i = 0, l = remainingNodes.length; i < l; ++i)
		{
			remainingNodes[i][this.TRAVERS_VISITED_KEY] = false;
		}

		while (remainingNodes.length)
		{
			var currNode;
			if (startingNode && (remainingNodes.indexOf(startingNode) >= 0))
				currNode = startingNode;
			else
				currNode = remainingNodes[0];

			var partialResult = this._doTravers(callback, currNode, breadthFirst, partialNodes);
			result.nodes = result.nodes.concat(partialResult.nodes);
			result.connectors = result.connectors.concat(partialResult.connectors);
			remainingNodes = AU.exclude(remainingNodes, partialResult.nodes);
		}
		return result;
	},
	/** @private */
	_doTravers: function(callback, startingNode, breadthFirst, partialNodes)
	{
		var result = {
			nodes: [],
			connectors: []
		};
		var node = startingNode;

		if (!node[this.TRAVERS_VISITED_KEY])
		{
			result.nodes.push(node);
			node[this.TRAVERS_VISITED_KEY] = true;
			if (callback)
				callback(node, false);
		}

		var connectors = AU.clone(node.getLinkedConnectors());
		var allConnectors = this.getConnectors();
		connectors.sort(function(c1, c2){
			return allConnectors.indexOf(c1) - allConnectors.indexOf(c2);
		});

		var unvisitedNodes = [];

		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			var connector = connectors[i];
			var neighborNodes = node.getLinkedObjsOnConnector(connector);
			if (partialNodes)
			  neighborNodes = AU.intersect(neighborNodes, partialNodes);

			for (var j = 0, k = neighborNodes.length; j <k; ++j)  // filter out the first node
			{
				var neighborNode = neighborNodes[j];  // TODO: only the normal molecule with two-end bond can be traversed
				if (neighborNode instanceof Kekule.BaseStructureNode)
					break;
			}

			if (!neighborNode)
				continue;

			if (!neighborNode[this.TRAVERS_VISITED_KEY])
			{
				result.nodes.push(neighborNode);
				result.connectors.push(connector);
				neighborNode[this.TRAVERS_VISITED_KEY] = true;
				if (callback)
				{
					callback(connector, true);
					callback(neighborNode, false);
				}

				if (breadthFirst)
					unvisitedNodes.push(neighborNode);
				else // depth first
				{
					var nextResult = this._doTravers(callback, neighborNode, breadthFirst, partialNodes);
					result.nodes = result.nodes.concat(nextResult.nodes);
					result.connectors = result.connectors.concat(nextResult.connectors);
				}
			}
		}

		if (breadthFirst)
		{
			for (var i = 0, l = unvisitedNodes.length; i < l; ++i)
			{
				var n = unvisitedNodes[i];
				var nextResult = this._doTravers(callback, n, breadthFirst, partialNodes);
				result.nodes = result.nodes.concat(nextResult.nodes);
				result.connectors = result.connectors.concat(nextResult.connectors);
			}
		}
		return result;
	}
});

/**
 * Represent an abstract container of nodes (molecule, substitution, group...) in chemical structure.
 * @class
 * @augments Kekule.ChemStructureNode
 * @param {String} id Id of this node.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {Kekule.MolecularFormula} formula Formula of this container.
 *   Usually used for some simple structures (such as inorganic molecules).
 *   Formual can also be calculated from {@link Kekule.StructureConnectionTable}.
 * @property {Kekule.StructureConnectionTable} ctab Connection table of this container.
 *   Usually used for some complex structures (such as organic molecules).
 * @property {Array} nodes All structure nodes in this fragment.
 * @property {Array} anchorNodes Nodes that can have bond connected to other structure nodes.
 * @property {Array} connectors Connectors (usually bonds) in this container.
 * @property {Array} crossConnectors Connectors outside the fragment connected to nodes inside fragment. Read only.
 * @property {Array} nonHydrogenNodes All structure nodes except hydrogen atoms in this fragment.
 * @property {Array} nonHydrogenConnectors Connectors except ones connected to hydrogen atoms in this fragment.
 * @property {Kekule.StructureFragmentShadow} flattenedShadow A shadow that "flatten" this structure fragment,
 *   unmarshalling all subgroups. Some algorithms (e.g., stereo detection) need to be carried out on flattened
 *   structure, this shadow may prevent the original structure from being modified.
 */
Kekule.StructureFragment = Class.create(Kekule.ChemStructureNode,
/** @lends Kekule.StructureFragment# */
{
	/** @private */
	CLASS_NAME: 'Kekule.StructureFragment',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id, coord2D, coord3D])  /* $super(id, coord2D, coord3D) */;
	},
	doFinalize: function(/*$super*/)
	{
		if (this.hasFormula())
			this.getFormula().finalize();
		if (this.hasCtab())
			this.getCtab().finalize();
		this.tryApplySuper('doFinalize')  /* $super() */;

		/*
		this.addEventListener('change', function(e){
			var target = e.target;
			if (target instanceof Kekule.ChemStructureObject)
			{
				// clear aromaticRings, change of node or connector may cause aromatic change
				this.setPropStoreFieldValue('aromaticRings', []);
			}
		});
		*/
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('formula', {
			'dataType': 'Kekule.MolecularFormula',
			'getter': function(allowCreate)
				{
					if (!this.getPropStoreFieldValue('formula'))
					{
						if (allowCreate)
						{
							this.createFormula();
						}
					}
					return this.getPropStoreFieldValue('formula');
				},
			'setter': function(value)
				{
					var old = this.getPropStoreFieldValue('formula');
					if (old)
					{
						old.finalize();
						old = null;
					}

					if (value)
					{
						value.setPropValue('parent', this, true);
						value.addEventListener('change', function(e){
							this.notifyPropSet('formula', this.getFormula());
						}, this);
					}

					this.setPropStoreFieldValue('formula', value);
				}
		});
		this.defineProp('ctab', {
			'dataType': 'Kekule.StructureConnectionTable',
			'getter': function(allowCreate)
				{
					if (!this.getPropStoreFieldValue('ctab'))
					{
						if (allowCreate)
							this.createCtab();
					}
					return this.getPropStoreFieldValue('ctab');
				},
			'setter': function(value)
			{
				var old = this.getPropStoreFieldValue('ctab');
				if (old)
				{
					old.finalize();
					old = null;
				}

				if (value)
				{
					value.setPropValue('parent', this, true);
					value.setOwner(this.getOwner());
					// install event listeners to ctab
					value.addEventListener('propValueSet',
						function(e)
						{
							if ((e.propName == 'nodes') || (e.propName == 'anchorNodes') || (e.propName == 'connectors'))
							{
								//console.log('mol prop set', e.propName, e.propValue);
								this.notifyPropSet(e.propName, e.propValue);
							}
						}, this);
					value.setEnablePropValueSetEvent(true); // to enable propValueSet event
				}

				this.setPropStoreFieldValue('ctab', value);
			}
		});
		// values are read from ctab
		this.defineProp('nodes', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLISHED,
			'setter': null,
			'getter': function() { return this.hasCtab()? this.getCtab().getNodes(): []; }
		});
		this.defineProp('anchorNodes', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLISHED,
			'setter': null,
			'getter': function() { return this.hasCtab()? this.getCtab().getAnchorNodes(): []; }
		});
		this.defineProp('connectors', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLISHED,
			'setter': null,
			'getter': function() { return this.hasCtab()? this.getCtab().getConnectors(): []; }
		});

		this.defineProp('crossConnectors', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'setter': null,
			'scope': Class.PropertyScope.PUBLIC,
			'getter': function()
				{
					//var result = [].concat(this.getLinkedConnectors());
					var result = [].concat(this.getPropStoreFieldValue('linkedConnectors'));
					// check external connectors of anchor nodes
					for (var i = 0, l = this.getNodeCount(); /*this.getAnchorNodeCount();*/ i < l; ++i)
					{
						//var connectors = this.getAnchorNodeAt(i).getLinkedConnectors();
						var node = this.getNodeAt(i);
						var connectors = node.getLinkedConnectors() || [];
						/*
						if (node.getCrossConnectors())
							Kekule.ArrayUtils.pushUnique(connectors, node.getCrossConnectors());
						*/
						for (var j = 0, k = connectors.length; j < k; ++j)
						{
							if (this.indexOfConnector(connectors[j]) < 0) // external connector
							{
								Kekule.ArrayUtils.pushUnique(result, connectors[j]);
							}
						}
					}
					return result;
				}
		});
		this.defineProp('nonHydrogenNodes', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC,
			'serializable': false,
			'setter': null,
			'getter': function()
			{
				return this.hasCtab()? this.getCtab().getNonHydrogenNodes(): [];
			}
		});
		this.defineProp('nonHydrogenConnectors', {
			'dataType': DataType.ARRAY,
			'scope': Class.PropertyScope.PUBLIC,
			'serializable': false,
			'setter': null,
			'getter': function()
			{
				return this.hasCtab()? this.getCtab().getNonHydrogenConnectors(): [];
			}
		});
		this.defineProp('canonicalizationInfo', {
			'dataType': DataType.OBJECT,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'getter': function()
			{
				return this.getStructureCacheData('canonicalizationInfo');
			},
			'setter': function(value)
			{
				this.setStructureCacheData('canonicalizationInfo', value);
			}
		});
		this.defineProp('aromaticRings', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'getter': function()
			{
				//var result = this.getPropStoreFieldValue('aromaticRings');
				var result = this.getStructureCacheData('aromaticRings');
				if (!result)
				{
					result = [];
					//this.setPropStoreFieldValue('aromaticRings', result);
					this.setStructureCacheData('aromaticRings', result);
				}
				return result;
			},
			'setter': function(value)
			{
				this.setStructureCacheData('aromaticRings', value);
			}
		});

		this.defineProp('flattenedShadow', {
			'dataType': 'Kekule.StructureFragmentShadow',
			'serializable': false,
			'scope': Class.PropertyScope.PRIVATE,
			'getter': function(autoCreate)
			{
				var result = null;
				if (Kekule.ObjUtils.isUnset(autoCreate))
					autoCreate = true;
				var shadowOnSelf = this.getFlattenedShadowOnSelf();
				if (shadowOnSelf)   // no explicit shadow object, shadow maps on self
					return null;
				else // has sub fragment, need explicit shadow object
				{
					result = this.getPropStoreFieldValue('flattenedShadow');
					if (!result && autoCreate)
					{
						//console.log('create shadow');
						result = new Kekule.StructureFragmentShadow(this);
						var shadowFragment = result.getShadowFragment();// this.getFlattenedShadowFragment();
						shadowFragment.unmarshalAllSubFragments(true);
						// copy structure info (e.g., ringInfo, aromatic info) to shadow
						this._copyAdditionalInfoToShadowFragment(shadowFragment, result.getSourceToShadowMap(), result.getShadowToSourceMap());
						this.setPropStoreFieldValue('flattenedShadow', result);
					}
				}

				return result;
			}
		});
		// a special property, marks that this fragment has no subgroup, and the flattenedShadow is actually map to itself
		this.defineProp('flattenedShadowOnSelf', {'dataType': DataType.BOOL, 'serializable': false, 'scope': Class.PropertyScope.PROVATE,
			'setter': null,
			'getter': function()
			{
				var result = this.setPropStoreFieldValue('flattenedShadowOnSelf');
				if (Kekule.ObjUtils.isUnset(result))
				{
					result = !this.hasSubFragments();
					this.setPropStoreFieldValue('flattenedShadowOnSelf', result);
				}
				return result;
			}
		});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'f';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['ctab', 'formula', 'nodes', 'anchorNodes', 'connectors']);
	},

	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		//console.log('do compare structure', options);
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			//if (this._getComparisonOptionFlagValue(options, 'atom'))
			{
				// TODO: now only check whether contains ctab or formula
				var hasCtab1 = this.hasCtab();
				var hasCtab2 = targetObj.hasCtab && targetObj.hasCtab();
				result = hasCtab1? (hasCtab2? 0: 1): (hasCtab2? -1: 0);
				if (!result)
				{
					var hasFormula1 = this.hasFormula();
					var hasFormula2 = targetObj.hasFormula && targetObj.hasFormula();
					result = hasFormula1? (hasFormula2? 0: 1): (hasFormula2? -1: 0);
				}

				// both has ctab, comparing child nodes and connectors
				if (!result && this.hasCtab())
				{
					if ((result === 0) && (this.getNonHydrogenNodes && targetObj.getNonHydrogenNodes))  // structure fragment, if with same node and connector count, compare nodes and connectors
					{
						var _getNeighorNodeIndexes = function(nodeOrConnector, parent)
						{
							var neighbors;
							if (nodeOrConnector instanceof Kekule.ChemStructureConnector)
								neighbors = nodeOrConnector.getConnectedNonHydrogenObjs();
							else if (nodeOrConnector instanceof Kekule.ChemStructureNode)
								neighbors = nodeOrConnector.getLinkedNonHydrogenObjs();  // ignore H
							var result = [];
							for (var i = 0, l = neighbors.length; i < l; ++i)
							{
								var n = neighbors[i];
								//if (n instanceof Kekule.ChemStructureNode)
								var index = parent.indexOfNode(n);
								if (index >= 0)  // ignore cross structure bonds
									result.push(index);
							}
							result.sort();
							return result;
						};


						var nodes1 = this.getNonHydrogenNodes();
						var nodes2 = targetObj.getNonHydrogenNodes();
						result = nodes1.length - nodes2.length;
						if (result === 0)
						{
							for (var i = 0, l = nodes1.length; i < l; ++i)
							{
								result = this.doCompareOnValue(nodes1[i], nodes2[i], options);
								if (result !== 0)
									break;
								else
								{
									// check the neighbor node index to current node, avoid issue #86
									var neighborNodeIndexes1 = _getNeighorNodeIndexes(nodes1[i], this);
									var neighborNodeIndexes2 = _getNeighorNodeIndexes(nodes2[i], targetObj);
									result = Kekule.ArrayUtils.compare(neighborNodeIndexes1, neighborNodeIndexes2);
									if (result !== 0)
									{
										//console.log('diff node', nodes1[i].getId(), neighborNodeIndexes1, nodes2[i].getId(), neighborNodeIndexes2);
										break;
									}
								}
							}
						}
					}
					if ((result === 0) && (this.getConnectors && targetObj.getConnectors))
					{
						var connectors1 = this.getNonHydrogenConnectors();
						var connectors2 = targetObj.getNonHydrogenConnectors();
						result = connectors1.length - connectors2.length;
						if (result === 0)
						{
							for (var i = 0, l = connectors1.length; i < l; ++i)
							{
								result = this.doCompareOnValue(connectors1[i], connectors2[i], options);
								if (result !== 0)
									break;
								else
								{
									// check the neighbor node index to current node, avoid issue #86
									var neighborNodeIndexes1 = _getNeighorNodeIndexes(connectors1[i], this);
									var neighborNodeIndexes2 = _getNeighorNodeIndexes(connectors2[i], targetObj);
									result = Kekule.ArrayUtils.compare(neighborNodeIndexes1, neighborNodeIndexes2);
									if (result !== 0)
									{
										//console.log('diff bond', connectors1[i].getId(), neighborNodeIndexes1, connectors2[i].getId(), neighborNodeIndexes2);
										break;
									}
								}
							}
						}
					}

					// The node/connector sequence check can distinguish most molecules
					// but a few of them (e.g. issue#74 https://github.com/partridgejiang/Kekule.js/issues/74)
					// still need a spanning tree check
					if (result === 0)
					{
						//console.log('spanning tree compare');
						// traverse from the last node, with all non hydrongen nodes
						var nonHydrogenNodesThis = this.getNonHydrogenNodes();
						var nonHydrogenNodesTarget = targetObj.getNonHydrogenNodes();
						var traversedObjsThis = this.traverse(null, nonHydrogenNodesThis[nonHydrogenNodesThis.length - 1], true, nonHydrogenNodesThis);
						var traversedObjsTarget = targetObj.traverse(null, nonHydrogenNodesTarget[nonHydrogenNodesTarget.length - 1], true, nonHydrogenNodesTarget);
						//console.log(traversedObjsThis.nodes, traversedObjsTarget.nodes);
						for (var i = 0, l = traversedObjsThis.nodes.length; i < l; ++i)
						{
							var nodeThis = traversedObjsThis.nodes[i];
							var nodeTarget = traversedObjsTarget.nodes[i];
							result = this.doCompareOnValue(nodeThis, nodeTarget, options);
							if (result !== 0)
								break;
						}
						if (result === 0)
						{
							for (var i = 0, l = traversedObjsThis.connectors.length; i < l; ++i)
							{
								var connectorThis = traversedObjsThis.connectors[i];
								var connectorTarget = traversedObjsTarget.connectors[i];
								result = this.doCompareOnValue(connectorThis, connectorTarget, options);
								if (result !== 0)
									break;
							}
						}
					}
				}
			}
		}
		return result;
	},

	/** @ignore */
	structureChange: function(/*$super, */originObj)
	{
		// when structure is changed, clear old shadow fragment
		this.setPropStoreFieldValue('flattenedShadowOnSelf', null);
		var shadow = this.getFlattenedShadow(false);
		if (shadow)
		{
			shadow.finalize();
			this.setPropStoreFieldValue('flattenedShadow', null);
		}
		// also clear all cached structure info data
		this.tryApplySuper('structureChange', [originObj])  /* $super(originObj) */;
	},
	/** @ignore */
	clearStructureFlags: function(/*$super*/)
	{
		this.tryApplySuper('clearStructureFlags')  /* $super() */;
		/*  Remove these, since now canonicalizationInfo and aromaticRings are already stored in structure cache
		this.setPropStoreFieldValue('canonicalizationInfo', null);
		this.setPropStoreFieldValue('aromaticRings', []);
		*/

		// also clear flags of children (e.g., parity of atoms and bonds)
		for (var i = 0, l = this.getChildCount(); i < l; ++i)
		{
			var c = this.getChildAt(i);
			if (c.clearStructureFlags)
				c.clearStructureFlags();
		}
	},
	/** @private */
	ownerChanged: function(/*$super, */newOwner, oldOwner)
	{
		if (this.hasCtab())
			this.getCtab().setOwner(newOwner);
		this.tryApplySuper('ownerChanged', [newOwner, oldOwner])  /* $super(newOwner, oldOwner) */;
	},
	/** @private */
	_removeChildObj: function(/*$super, */obj)
	{
		if (this.hasFormula())
		{
			var formula = this.getFormula();
			if (formula === obj)
			{
				this.removeFormula();
			}
		}
		else if (this.hasCtab())
		{
			var ctab = this.getCtab();
			if (ctab === obj)
				this.removeCtab();
			else
			{
				if (ctab.hasChildObj(obj))
					ctab.removeChildObj(obj);
			}
		}
		this.tryApplySuper('_removeChildObj', [obj])  /* $super(obj) */;
	},

	/**
	 * Returns if this fragment has no formula or ctab, or ctab has no nodes or connectors.
	 * @return {Bool}
	 */
	isEmpty: function()
	{
		//var result = $super();
		//if (result)
		var result;
		{
			result = !this.hasFormula();
			if (result)
			{
				result = !this.hasCtab();
				if (!result)  // check if ctab is empty
				{
					result = this.getCtab().isEmpty();
				}
			}
		}
		return result;
	},
	/**
	 * Returns if the ctab of this structure fragment has no nodes or connectors.
	 * @return {Bool}
	 */
	isCtabEmpty: function()
	{
		return !this.hasCtab() || this.getCtab().isEmpty();
	},

	/** @ignore */
	doGetLinkedConnectors: function()
	{
		return this.getCrossConnectors();
	},
	/** @ignore */
	appendLinkedConnector: function(/*$super, */connector)
	{
		var actualLinkedNode = this.getCurrConnectableObj();
		if (actualLinkedNode && actualLinkedNode !== this) // instead of link connector to self, we'd rather link connector to child anchor node
			return actualLinkedNode.appendLinkedConnector(connector);
		else  // no child, link to self
		  return this.tryApplySuper('appendLinkedConnector', [connector])  /* $super(connector) */;
	},

	/** @ignore */
	getCurrConnectableObj: function()
	{
		// instead of link connector to self, we'd rather link connector to child anchor node
		var candidates = (this.getAnchorNodeCount() > 0)? this.getAnchorNodes(): this.getNodes();
		if (candidates.length <= 0)
			return this;
		else  // iterate all candidates, find the first one with min cross connector count
		{
			var allCrossConnectors = this.getCrossConnectors();
			var minCrossConnectorCount = null;
			var minNode = null;
			for (var i = 0, l = candidates.length; i < l; ++i)
			{
				var node = candidates[i];
				var connectors = node.getLinkedConnectors();
				var crossConnectors = Kekule.ArrayUtils.intersect(connectors, allCrossConnectors);
				var crossConnectorCount = crossConnectors.length;
				if (minCrossConnectorCount === null || minCrossConnectorCount > crossConnectorCount)
				{
					minCrossConnectorCount = crossConnectorCount;
					minNode = node;
				}
			}
			return minNode;
		}
		//return this.getAnchorNodeAt(0) || this.getNodeAt(0) || this.getChildAt(0) || this;
	},
	/** @ignore */
	getActualConnectedObjToConnector: function(connector)
	{
		// ensure connector is not a child connector of self
		if (this.indexOfConnector(connector) >= 0)  // internal connector, bypass
			return null;
		else
		{
			if (connector.indexOfConnectedObj(this) >= 0)  // directly linked to self, not child nodes
				return this;
			// check child nodes
			for (var i = 0, l = connector.getConnectedObjCount(); i < l; ++i)
			{
				var obj = connector.getConnectedObjAt(i);
				if (obj.isChildOf(this))
					return obj;
			}
			return null;
		}
	},

	/** @private */
	createCtab: function()
	{
		var ctab = new Kekule.StructureConnectionTable(this.getOwner(), this);
		this.setCtab(ctab);
	},
	/** @private */
	createFormula: function()
	{
		var formula = new Kekule.MolecularFormula(this);
		this.setFormula(formula);
	},
	/*
	// Notify {@link Kekule.StructureFragment#nodes} property has been changed
	notifyNodesChanged: function()
	{
		this.notifyPropSet('nodes', this.getPropStoreFieldValue('nodes'));
	},
	// Notify {@link Kekule.StructureFragment#anchorNodes} property has been changed
	notifyAnchorNodesChanged: function()
	{
		this.notifyPropSet('anchorNodes', this.getPropStoreFieldValue('anchorNodes'));
	},
	// Notify {@link Kekule.StructureFragment#connectors} property has been changed
	notifyConnectorsChanged: function()
	{
		this.notifyPropSet('connectors', this.getPropStoreFieldValue('connectors'));
	},
	*/

	/**
	 * Check whether a connection table is used to represent this fragment.
	 */
	hasCtab: function()
	{
		return (!!this.getPropStoreFieldValue('ctab'));
	},
	/**
	 * Check whether a formula is used to represent this fragment.
	 */
	hasFormula: function()
	{
		return (!!this.getPropStoreFieldValue('formula'));
	},

	/**
	 * Delete formula info from this fragment.
	 */
	removeFormula: function()
	{
		var f = this.getPropStoreFieldValue('formula');
		if (f)
		{
			f.finalize();
			f = null;
			this.setPropStoreFieldValue('formula', null);
		}
		return this;
	},
	/**
	 * Delete ctab info from this fragment.
	 */
	removeCtab: function()
	{
		var f = this.getPropStoreFieldValue('ctab');
		if (f)
		{
			f.finalize();
			f = null;
			this.setPropStoreFieldValue('ctab', null);
		}
		return this;
	},

	/**
	 * Calculate the box to fit all sub molecule.
	 * @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
	 * @param {Bool} allowCoordBorrow
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox: function(/*$super, */coordMode, allowCoordBorrow)
	{
		if (this.hasCtab())
		{
			return this.getCtab().getContainerBox(coordMode, allowCoordBorrow);
		}
		else
			return this.tryApplySuper('getContainerBox', [coordMode])  /* $super(coordMode) */;
	},

	/**
	 * Get a structure node object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.BaseStructureNode}
	 */
	getNodeById: function(id)
	{
		/*
		var nodes = this.getNodes();
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			if (nodes[i].getId() == id)
				return nodes[i];
		}
		return null;
		*/
		return this.hasCtab()? this.getCtab().getNodeById(id): null;
	},
	/**
	 * Get a structure connector object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.BaseStructureConnector}
	 */
	getConnectorById: function(id)
	{
		/*
		var connectors = this.getConnectors();
		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			if (connectors[i].getId() == id)
				return connectors[i];
		}
		return null;
		*/
		return this.hasCtab()? this.getCtab().getConnectorById(id): null;
	},
	/**
	 * Get a structure node or connector object with a specified id.
	 * @param {String} id
	 * @returns {Kekule.ChemStructureObject}
	 */
	getObjectById: function(id)
	{
		var node = this.getNodeById(id);
		return node? node: this.getConnectorById(id);
	},
	/**
	 * Return count of nodes.
	 * @returns {Int}
	 */
	getNodeCount: function()
	{
		return this.hasCtab()? this.getCtab().getNodeCount(): 0;
	},
	/**
	 * Get node at index.
	 * @param {Int} index
	 * @returns {Kekule.BaseStructureNode}
	 */
	getNodeAt: function(index)
	{
		return this.hasCtab()? this.getCtab().getNodeAt(index): null;
	},
	/**
	 * Get index of node.
	 * @param {Kekule.BaseStructureNode} node
	 * @returns {Int}
	 */
	indexOfNode: function(node)
	{
		return this.hasCtab()? this.getCtab().indexOfNode(node): -1;
	},
	/**
	 * Check if a node exists in structure.
	 * @param {Kekule.BaseStructureNode} node Node to seek.
	 * @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
	 * @returns {Bool}
	 */
	hasNode: function(node, checkNestedStructure)
	{
		return this.hasCtab()? this.getCtab().hasNode(node, checkNestedStructure): null;
	},
	/**
	 * Returns index of node. If node exists in nested sub group, index in sub group will be pushed to stack as well.
	 * @param {Kekule.BaseStructureNode} node
	 * @returns {Variant} If node is the direct child of this structure, returns {Int}, otherwise stack {Array} will be returned.
	 */
	indexStackOfNode: function(node)
	{
		return this.hasCtab()? this.getCtab().indexStackOfNode(node): -1;
	},
	/**
	 * Get node at indexStack.
	 * For example, indexStack is [2, 3, 1], then this.getNodeAt(2).getNodeAt(3).getNodeAt(1) will be returned.
	 * @param {Array} indexStack Array of integers.
	 * @returns {Kekule.BaseStructureNode}
	 */
	getNodeAtIndexStack: function(indexStack)
	{
		return this.hasCtab()? this.getCtab().getNodeAtIndexStack(indexStack): null;
	},
	/**
	 * Returns direct child object of ctab which hold node as nested child.
	 * @returns {Kekule.StructureFragment}
	 */
	getDirectChildOfNestedNode: function(node)
	{
		var p = node.getParent();
		if (!p)
			return null;
		else if (p === this)
			return p;
		else
		{
			var curr = node;
			while (p && (p.getParent) && (p !== this))
			{
				curr = p;
				p = p.getParent();
			}
			if (p === this)
				return curr;
			else
				return null;
		}
	},

	/**
	 * Insert a node before refNode.
	 * @param {Kekule.SimpleStructureConnector} node
	 * @param {Kekule.SimpleStructureConnector} refNode
	 */
	insertNodeBefore: function(node, refNode)
	{
		return this.doGetCtab(true).insertNodeBefore(node, refNode);
	},
	/**
	 * Add node to container. If node already in container, nothing will be done.
	 * @param {Kekule.BaseStructureNode} node
	 */
	appendNode: function(node)
	{
		/*
		if (this.getNodes().indexOf(node) >= 0) // already exists
			;// do nothing
		else
		{
			var result = this.getNodes().push(node);
			this.notifyNodesChanged();
			return result;
		}
		*/
		return this.doGetCtab(true).appendNode(node);
	},
	/**
	 * Insert node to index. If index is not set, node will be inserted as the first node of ctab.
	 * @param {Kekule.BaseStructureNode} node
	 * @param {Int} index
	 */
	insertNodeAt: function(node, index)
	{
		return this.doGetCtab(true).insertNodeAt(node, index);
	},
	/**
	 * Remove node at index in container.
	 * @param {Int} index
	 * @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
	 */
	removeNodeAt: function(index, preserveLinkedConnectors)
	{
		/*
		var node = this.getNodes()[index];
		if (node)
		{
			// remove from connectors
			this.removeConnectNode(node);
			this.getNodes().removeAt(index);
			this.notifyNodesChanged();
		}
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeNodeAt(index, preserveLinkedConnectors);
	},
	/**
	 * Remove a node in container.
	 * @param {Kekule.BaseStructureNode} node
	 * @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
	 */
	removeNode: function(node, preserveLinkedConnectors)
	{
		/*
		var index = this.getNodes().indexOf(node);
		if (index >= 0)
			this.removeNodeAt(index);
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeNode(node, preserveLinkedConnectors);
	},
	/**
	 * Replace oldNode with new one, preserve coords and all linked connectors.
	 * @param {Kekule.BaseStructureNode} oldNode Must be direct child of current fragment (node in nested structure fragment will be ignored).
	 * @param {Kekule.BaseStructureNode} newNode
	 */
	replaceNode: function(oldNode, newNode)
	{
		if (!this.hasCtab())
			return null;
		return this.getCtab().replaceNode(oldNode, newNode);
	},
	/**
	 * Remove all nodes.
	 */
	clearNodes: function()
	{
		if (this.hasCtab())
			return this.getCtab().clearNodes();
	},
	/**
	 * Sort direct child nodes in structure fragment.
	 * @param {Function} sortFunc Function to determine the priority of nodes.
	 */
	sortNodes: function(sortFunc)
	{
		if (this.hasCtab())
			return this.getCtab().sortNodes(sortFunc);
	},
	/**
	 * Check if child nodes has 2D coord.
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoord2D: function(allowCoordBorrow, checkZeroCoord)
	{
		if (!this.hasCtab())
			return false;
		return this.getCtab().nodesHasCoord2D(allowCoordBorrow, checkZeroCoord);
	},
	/**
	 * Check if child nodes has 3D coord.
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoord3D: function(allowCoordBorrow, checkZeroCoord)
	{
		if (!this.hasCtab())
			return false;
		return this.getCtab().nodesHasCoord3D(allowCoordBorrow, checkZeroCoord);
	},
	/**
	 * Check if child nodes has coord on coordMode.
	 * @param {Int} coordMode
	 * @param {Bool} allowCoordBorrow
	 * @param {Bool} checkZeroCoord If true, when more than one nodes has coord (0, 0, 0), those nodes will be considered as without coord.
	 * @returns {Bool}
	 */
	nodesHasCoordOfMode: function(coordMode, allowCoordBorrow, checkZeroCoord)
	{
		if (!this.hasCtab())
			return false;
		return this.getCtab().nodesHasCoordOfMode(coordMode, allowCoordBorrow, checkZeroCoord);
	},

	/**
	 * Check if a node is in aromatic ring stored in aromaticRings property.
	 * @param {Kekule.BaseStructureNode} node
	 * @returns {Bool}
	 */
	isNodeInAromaticRing: function(node)
	{
		var rings = this.getAromaticRings();
		for (var i = 0, l = rings.length; i < l; ++i)
		{
			var nodes = rings[i].nodes;
			if (nodes.indexOf(node) >= 0)
				return true;
		}
		return false;
	},

	/**
	 * Util method to create a new Kekule.Atom instance and append to current connection table.
	 * @param {Variant} elemSymbolOrAtomicNumber
	 * @param {Number} massNumber
	 * @param {Hash} coord2D
	 * @param {Hash} coord3D
	 * @returns {Kekule.Atom}
	 * @private
	 */
	appendAtom: function(elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D)
	{
		return this.doGetCtab(true).appendAtom(elemSymbolOrAtomicNumber, massNumber, coord2D, coord3D);
	},

	/**
	 * Return count of anchorNodes.
	 * @returns {Int}
	 */
	getAnchorNodeCount: function()
	{
		return this.hasCtab()? this.getCtab().getAnchorNodeCount(): 0;
	},
	/**
	 * Get anchor node at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemStructureNode}
	 */
	getAnchorNodeAt: function(index)
	{
		//return this.getAnchorNodes()[index];
		return this.hasCtab()? this.getCtab().getAnchorNodeAt(index): null;
	},
	/**
	 * Get index of anchor node.
	 * @param {Kekule.ChemStructureNode} node
	 * @returns {Int}
	 */
	indexOfAnchorNode: function(node)
	{
		return this.hasCtab()? this.getCtab().indexOfAnchorNode(node): -1;
	},
	/**
	 * Add anchor node of container. If node not in nodes container, nothing will be done.
	 * @param {Kekule.ChemStructureNode} node
	 */
	appendAnchorNode: function(node)
	{
		/*
		if (this.getNodes().indexOf(node) >= 0)
		{
			if (this.getAnchorNodes().indexOf(node) < 0)
			{
				this.getAnchorNodes().push(node);
				this.notifyAnchorNodesChanged();
			}
		}
		*/
		return this.doGetCtab(true).appendAnchorNode(node);
	},
	/**
	 * Remove node at index of anchorNodes.
	 * @param {Int} index
	 */
	removeAnchorNodeAt: function(index)
	{
		/*
		var node = this.getAnchorNodes()[index];
		if (node)
		{
			this.getAnchorNodes().removeAt(index);
			this.notifyAnchorNodesChanged();
		}
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeAnchorNodeAt(index);
	},
	/**
	 * Remove a node in anchorNodes.
	 * @param {Kekule.ChemStructureNode} node
	 */
	removeAnchorNode: function(node)
	{
		/*
		var index = this.getAnchorNodes().indexOf(node);
		if (index >= 0)
			this.removeAnchorNodeAt(index);
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeAnchorNode(node);
	},
	/**
	 * Remove all anchor nodes.
	 */
	clearAnchorNodes: function()
	{
		if (this.hasCtab())
			return this.getCtab().clearAnchorNodes();
	},
	/**
	 * Return count of connectors.
	 * @returns {Int}
	 */
	getConnectorCount: function()
	{
		return this.hasCtab()? this.getCtab().getConnectorCount(): 0;
	},
	/**
	 * Get connector at index.
	 * @param {Int} index
	 * @returns {Kekule.BaseStructureConnector}
	 */
	getConnectorAt: function(index)
	{
		//return this.getConnectors()[index];
		return this.hasCtab()? this.getCtab().getConnectorAt(index): null;
	},
	/**
	 * Get index of connector inside fragment.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @returns {Int}
	 */
	indexOfConnector: function(connector)
	{
		return this.hasCtab()? this.getCtab().indexOfConnector(connector): -1;
	},
	/**
	 * Check if a connector exists in structure.
	 * @param {Kekule.BaseStructureConnector} connector Connector to seek.
	 * @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
	 * @returns {Bool}
	 */
	hasConnector: function(connector, checkNestedStructure)
	{
		return this.hasCtab()? this.getCtab().hasConnector(connector, checkNestedStructure): null;
	},

	/**
	 * Insert a connector before refConnector.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Kekule.BaseStructureConnector} refConnector
	 */
	insertConnectorBefore: function(connector, refConnector)
	{
		return this.doGetCtab(true).insertConnectorBefore(connector, refConnector);
	},
	/**
	 * Add connector to container.
	 * @param {Kekule.BaseStructureConnector} connector
	 */
	appendConnector: function(connector)
	{
		/*
		if (this.getConnectors().indexOf(connector) >= 0) // already exists
			;// do nothing
		else
		{
			return this.getConnectors().push(connector);
			this.notifyConnectorsChanged();
		}
		*/
		return this.doGetCtab(true).appendConnector(connector);
	},
	/**
	 * Insert connector to index. If index is not set, node will be inserted as the first connector of ctab.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Int} index
	 */
	insertConnectorAt: function(connector, index)
	{
		return this.doGetCtab(true).insertConnectorAt(connector, index);
	},
	/**
	 * Remove connector at index of connectors.
	 * @param {Int} index
	 * @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
	 */
	removeConnectorAt: function(index, preserveConnectedObjs)
	{
		/*
		var connector = this.getConnectors()[index];
		if (connector)
		{
			this.getConnectors().removeAt(index);
			this.notifyConnectorsChanged();
		}
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeConnectorAt(index, preserveConnectedObjs);
	},
	/**
	 * Remove a connector in container.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
	 */
	removeConnector: function(connector, preserveConnectedObjs)
	{
		/*
		var index = this.getConnectors().indexOf(connector);
		if (index >= 0)
			this.removeConnectorAt(index);
		*/
		if (!this.hasCtab())
			return null;
		return this.getCtab().removeConnector(connector, preserveConnectedObjs);
	},
	/**
	 * Remove all connectors.
	 */
	clearConnectors: function()
	{
		if (this.hasCtab())
			return this.getCtab().clearConnectors();
	},
	/**
	 * Sort direct child connectors in structure fragment.
	 * @param {Function} sortFunc Function to determine the priority of nodes.
	 */
	sortConnectors: function(sortFunc)
	{
		if (this.hasCtab())
			return this.getCtab().sortConnectors(sortFunc);
	},

	/**
	 * Check if a connector is in aromatic ring stored in aromaticRings property.
	 * @param {Kekule.BaseStructureConnector} connector
	 * @returns {Bool}
	 */
	isConnectorInAromaticRing: function(connector)
	{
		var rings = this.getAromaticRings();
		for (var i = 0, l = rings.length; i < l; ++i)
		{
			var connectors = rings[i].connectors;
			if (connectors.indexOf(connector) >= 0)
				return true;
		}
		return false;
	},

	/**
	 * A util method to create a new bond object connected with nodes and append to current connection table.
	 * @param {Array} nodesOrIndexes Array of connected nodes or indexes of those nodes
	 * @param {Int} bondOrder Order of bond.
	 * @param {Int} bondType Type of bond.
	 * @returns {Kekule.Bond}
	 */
	appendBond: function(nodesOrIndexes, bondOrder, bondType)
	{
		return this.doGetCtab(true).appendBond(nodesOrIndexes, bondOrder, bondType);
	},

	/*
	 * Insert obj before refChild in node or connector list of ctab.
	 * If refChild is null or does not exists, obj will be append to tail of list.
	 * @param {Variant} obj A node or connector.
	 * @param {Variant} refChild Ref node or connector
	 * @return {Int} Index of obj after inserting.
	 */
	/*
	insertBefore: function($super, obj, refChild)
	{
		var result = -1;
		if (this.hasCtab()
				&& (obj instanceof Kekule.ChemStructureObject) && (!refChild || refChild instanceof Kekule.ChemStructureObject))
			result = this.getCtab().insertBefore(obj, refChild);
		if (result < 0)
			result = $super(obj, refChild);
		return result;
	},
  */

	/**
	 * Returns nodes or connectors that should be removed cascadely with childObj.
	 * @param {Object} childObj
	 * @returns {Array}
	 * @private
	 */
	_getObjsNeedToBeCascadeRemoved: function(childObj, ignoredChildObjs)
	{
		if (this.hasCtab())
			return this.getCtab()._getObjsNeedToBeCascadeRemoved(childObj, ignoredChildObjs);
		else
			return [];
	},

	/**
	 * Remove childObj from connection table.
	 * @param {Variant} childObj A child node or connector.
	 * @param {Bool} cascadeRemove Whether remove related objects (e.g., bond connected to an atom).
	 * @param {Bool} freeRemoved Whether free all removed objects.
	 */
	removeChildObj: function(childObj, cascadeRemove, freeRemoved)
	{
		if (this.hasCtab())
			this.getCtab().removeChildObj(childObj, cascadeRemove, freeRemoved);
	},
	/*
	 * Remove child obj directly from connection table.
	 * @param {Variant} childObj A child node or connector.
	 */
	/*
	removeChild: function($super, obj)
	{
		return this.removeChildObj(obj) || $super(obj);
	},
	*/

	/**
	 * Check if childObj is a child node or connector of this fragment's ctab.
	 * @param {Kekule.ChemObject} childObj
	 * @returns {Bool}
	 */
	hasChildObj: function(childObj)
	{
		if (this.hasCtab())
		{
			return this.getCtab().hasChildObj(childObj);
		}
		else
			return false;
	},

	/**
	 * Returns next sibling node or connector to childObj.
	 * @param {Variant} childObj Node or connector.
	 * @returns {Variant}
	 */
	getNextSiblingOfChild: function(/*$super, */childObj)
	{
		if (this.hasCtab())
			return this.getCtab().getNextSiblingOfChild(childObj) || this.tryApplySuper('getNextSiblingOfChild', [childObj])  /* $super(childObj) */;
		else
			return this.tryApplySuper('getNextSiblingOfChild', [childObj])  /* $super(childObj) */;
	},

	/** @ignore */
	getChildSubgroupNames: function(/*$super*/)
	{
		return ['node', 'connector'].concat(this.tryApplySuper('getChildSubgroupNames')  /* $super() */);
	},
	/** @ignore */
	getBelongChildSubGroupName: function(/*$super, */obj)
	{
		if (obj instanceof Kekule.SimpleStructureNode)
			return 'node';
		else if (obj instanceof Kekule.BaseStructureConnector)
			return 'connector';
		else
			return this.tryApplySuper('getBelongChildSubGroupName', [obj])  /* $super(obj) */;
	},

	/*
	 * Get count of child objects (including both nodes and connectors).
	 * @returns {Int}
	 */
	/*
	getChildCount: function($super)
	{
		if (this.hasCtab())
			return this.getCtab().getChildCount() + $super();
		else
			return $super(); // 0;
	},
	*/
	/*
	 * Get child object (including both nodes and connectors) at index.
	 * @param {Int} index
	 * @returns {Variant}
	 */
	/*
	getChildAt: function($super, index)
	{
		if (this.hasCtab())
			return this.getCtab().getChildAt(index) || $super(index - this.getCtab().getChildCount());
		else
			return $super(index);
	},
	*/
	/*
	 * Get the index of obj in children list.
	 * @param {Variant} obj
	 * @returns {Int} Index of obj or -1 when not found.
	 */
	/*
	indexOfChild: function($super, obj)
	{
		var result = -1;
		if (this.hasCtab())
		{
			result = this.getCtab().indexOfChild(obj);
			if (result < 0)
				result = $super(obj) + this.getCtab().getChildCount();
		}
		else
			result = $super(obj);
		return result;
	},
	*/

	/**
	 * Check if a node has a sub structure (has child nodes).
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @param {Kekule.ChemStructureNode} node
	 * @returns {Bool}
	 */
	isSubFragment: function(node)
	{
		return (node.getNodeCount && (node.getNodeCount() > 0));
	},
	/**
	 * Get all sub fragments (node that have children, usually SubGroup).
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @returns {Array} Array of {@link Kekule.StructureFragment}.
	 */
	getSubFragments: function()
	{
		return this.hasCtab()? this.getCtab().getSubFragments(): [];
	},
	/**
	 * Returns whether there are sub fragment(s) (node that have children, usually SubGroup) in this fragment.
	 * Note if a sub group has no children, it will not be regarded as sub fragment.
	 * @returns {Bool}
	 */
	hasSubFragments: function()
	{
		return this.hasCtab()? this.getCtab().hasSubFragments(): false;
	},
	/**
	 * Get all leaf nodes (node that do not have children, usually atom).
	 * Note if a sub group has no children, it will be regarded as leaf node too.
	 * @returns {Array} Array of {@link Kekule.BaseStructureNode}.
	 */
	getLeafNodes: function()
	{
		/*
		var result = [];
		for (var i = 0, l = this.getNodeCount(); i < l; ++i)
		{
			var node = this.getNodeAt(i);
			if (!this.isSubFragment(node))
				result.push(node);
			else if (node.getLeafNodes)
			{
				var leafs = node.getLeafNodes();
				result = result.concat(leafs);
			}
		}
		return result;
		*/
		return this.hasCtab()? this.getCtab().getLeafNodes(): [];
	},
	/**
	 * Returns the direct node/substructure that contains originObj.
	 * originObj may be a child node or connector of substructure in this structure fragment.
	 * If originObj is actually not in this ctab, null will be returned.
	 * @param {Kekule.BaseStructureObject} originObj
	 * @returns {Kekule.ChemStructureNode}
	 */
	findDirectChildOfObj: function(originObj)
	{
		return this.hasCtab()? this.getCtab().findDirectChildOfObj(originObj): null;
	},
	/**
	 * Return all bonds in structure as well as in sub structure.
	 * @returns {Array} Array of {Kekule.BaseStructureConnector}.
	 */
	getAllChildConnectors: function()
	{
		/*
		var result = [].concat(this.getConnectors());
		var subFrags = this.getSubFragments();
		for (var i = 0, l = subFrags.length; i < l; ++i)
		{
			if (subFrags[i].getAllChildConnectors)
				result = result.concat(subFrags[i].getAllChildConnectors());
		}
		return result;
		*/
		return this.hasCtab()? this.getCtab().getAllChildConnectors(): [];
	},
	/**
	 * Return all bonds in structure as well as in sub structure.
	 * @returns {Array} Array of {Kekule.BaseStructureConnector}.
	 */
	getAllContainingConnectors: function()
	{
		return this.hasCtab()? this.getCtab().getAllContainingConnectors(): [];
	},

	/**
	 * Returns an array of all isotope, count and charge map.
	 * @private
	 */
	getIsotopeMaps: function()
	{
		return this.hasCtab()? this.getCtab().getIsotopeMaps(): [];
	},

	/**
	 * Clear all CTab and formula struuctures.
	 */
	clear: function()
	{
		if (this.hasCtab())
			this.setCtab(undefined);
		if (this.hasFormula())
			this.setFormula(undefined);
	},

	/**
	 * Clean the structure in ctab, removing illegal nodes and connectors.
	 * @param {Hash} options An option hash, can including fields: {orphanChemNode, hangingChemConnector}.
	 */
	clean: function(options)
	{
		var op = Object.extend({}, Kekule.globalOptions.algorithm.structureClean.structureCleanOptions);
		op = Object.extend(op, options || {});

		return this.doClean(op);
	},
	/** @private */
	doClean: function(options)
	{
		if (options.hangingChemConnector)
		{
			for (var i = this.getConnectorCount() - 1; i >= 0; --i)
			{
				var conn = this.getConnectorAt(i);
				if (conn instanceof Kekule.ChemStructureConnector)
				{
					if (conn.getConnectedObjCount() < 2)
					{
						this.removeConnectorAt(i);
					}
				}
			}
		}

		var nodeCount = this.getNodeCount();
		for (var i = nodeCount - 1; i >= 0; --i)
		{
			var node = this.getNodeAt(i);
			if (node instanceof Kekule.ChemStructureNode)
			{
				if (this.isSubFragment(node) && node.doClean)
				{
					node.doClean(options);
				}
				else
				{
					if (options.orphanChemNode)
					{
						if (node.getLinkedConnectorCount() < 1 && nodeCount > 1)  // if only one node in structure, do not clean it
						{
							this.removeNodeAt(i);
						}
					}
				}
			}
		}

		return this;
	},

	/**
	 * Removes all explicit hydrogen atoms and related bonds from this structure.
	 */
	clearExplicitBondHydrogens: function(forceKeepStructureCache, forceClearStereoHydrogenAtoms)
	{
		var self = this;
		var isHydrogenConnectedWithSimpleSingleBond = function(node)
		{
			var connectors = node.getLinkedConnectors();
			var result = connectors.length === 1;
			if (result)
			{
				var connector = connectors[0];
				result = connector.isSingleBond && connector.isSingleBond()
					&& connector.getStereo && (!connector.getStereo() || connector.getStereo() === Kekule.BondStereo.NONE);
			}
			return result;
		};
		var delNodeWithConnectors = function(node, index)
		{
			var conns = node.getLinkedConnectors();
			for (var i = conns.length; i >= 0; --i)
			{
				var conn = conns[i];
				if (self.hasConnector(conn, true))
					self.removeConnector(conn, false);
			}
			self.removeNodeAt(index, false);
		};
		var delHydrogenAtomWithConnectors = function(atom, index, addHCountCache)
		{
			var conns = atom.getLinkedConnectors();
			for (var i = conns.length; i >= 0; --i)
			{
				var conn = conns[i];
				if (self.hasConnector(conn, true))
				{
					if (addHCountCache)  // mark the count of bonded H atom in structure cache
					{
						var node = atom.getLinkedObjsOnConnector(conn)[0];
						var oldCount = node.getStructureCacheData('omittedBondHydrogenAtomCount') || 0;
						node.setStructureCacheData('omittedBondHydrogenAtomCount', oldCount + 1);
					}
					self.removeConnector(conn, false);
				}
			}
			self.removeNodeAt(index, false);
		};

		if (forceKeepStructureCache)
		{
			var oldAutoClear = this.getAutoClearStructureCache();
			this.setAutoClearStructureCache(false);  // prevent structure cache be cleared when removing H atoms
		}
		this.beginUpdate();
		try
		{
			var nodeCount = this.getNodeCount();
			for (var i = nodeCount - 1; i >= 0; --i)
			{
				var node = this.getNodeAt(i);
				if (node instanceof Kekule.ChemStructureNode)
				{
					if (this.isSubFragment(node) && node.clearExplicitBondHydrogens)
					{
						node.clearExplicitBondHydrogens(forceKeepStructureCache, forceClearStereoHydrogenAtoms);
						if (node.getNodeCount() <= 0)  // after clear, no atom exists in this subgroup
						{
							delNodeWithConnectors(node, i);
						}
					}
					else
					{
						if (node.isHydrogenAtom())
						{
							if (forceClearStereoHydrogenAtoms || isHydrogenConnectedWithSimpleSingleBond(node))
							{
								//delNodeWithConnectors(node, i);
								delHydrogenAtomWithConnectors(node, i, true);
							}
						}
					}
				}
			}
		}
		finally
		{
			this.endUpdate();
			if (forceKeepStructureCache)
				this.setAutoClearStructureCache(oldAutoClear);
		}
		return this;
	},

	/**
	 * Calculate molecular formula from this connection table.
	 * @returns {Kekule.MolecularFormula}
	 */
	calcFormula: function()
	{
		if (this.hasCtab())
			return this.getCtab().calcFormula();
		else if (this.hasFormula())
			return this.getFormula();
		else
			return null;
	},

	/**
	 * Group up nodes and turn them into a new sub {@link Kekule.StructureFragment}.
	 * @param {Array} groupNodes A set of {@link Kekule.ChemStructureNode}, must in target structure.
	 * @param {Kekule.StructureFragment} subFragment new sub group.
	 * @returns {Kekule.StructureFragment}
	 */
	marshalSubFragment: function(groupNodes, subFragment)
	{
		if (this.hasCtab())
		{
			this.beginUpdate();
			try
			{
				this.appendNode(subFragment);
				var nodes = [];
				var anchors = [];
				var innerConnectors = [];
				var externalConnectors = [];
				// find out how many nodes and connectors should be moved to subFragment
				for (var i = 0, l = groupNodes.length; i < l; ++i)
				{
					var node = groupNodes[i];
					if (this.indexOfNode(node) < 0) // node not in this, bypass
						continue;
					var isAnchor = false;
					var connectors = node.getLinkedConnectors();
					for (var j = 0, k = connectors.length; j < k; ++j)
					{
						var connector = connectors[j];
						var isExternalConnector = false;
						var objs = connector.getConnectedObjs();
						for (var m = 0, n = objs.length; m < n; ++m)
						{
							var obj = objs[m];
							if (obj != node)
							{
								if (groupNodes.indexOf(obj) < 0) // has link external to nodes, this node should be an anchor
								{
									//anchors.push(node);
									isAnchor = true;
									isExternalConnector = true;
								}
							}
						}
						if (isExternalConnector)
							Kekule.ArrayUtils.pushUnique(externalConnectors, connector);
						else
							Kekule.ArrayUtils.pushUnique(innerConnectors, connector);
					}
					Kekule.ArrayUtils.pushUnique(nodes, node);
					if (isAnchor)
						Kekule.ArrayUtils.pushUnique(anchors, node);
				}

				// TODO: need test here
				// then remove node and internal connectors from original structure, but preserve relations
				for (var i = innerConnectors.length - 1; i >= 0; --i)
					this.removeConnector(innerConnectors[i], true);
				for (var i = nodes.length - 1; i >= 0; --i)
					this.removeNode(nodes[i], true);
				// then add them to the new subFragment
				subFragment.beginUpdate();
				try
				{
					for (var i = 0, l = nodes.length; i < l; ++i)
						subFragment.appendNode(nodes[i]);
					for (var i = 0, l = anchors.length; i < l; ++i)
						subFragment.appendAnchorNode(anchors[i]);
					for (var i = 0, l = innerConnectors.length; i < l; ++i)
						subFragment.appendConnector(innerConnectors[i]);
				}
				finally
				{
					subFragment.endUpdate();
				//subFragment.recalcCoords();
				}
			}
			finally
			{
				this.endUpdate();
			}

			return subFragment;
		}
		else
			return null;
	},
	/**
	 * Remove sub {@link Kekule.StructureFragment} and move its nodes and connectors into this fragment.
	 * @param {Kekule.StructureFragment} subFragment Sub fragment to be ungrouped.
	 * @param {Bool} cascade If subfragment should also unmarshal its children fragments.
	 */
	unmarshalSubFragment: function(subFragment, cascade)
	{
		if (this.hasCtab())
		{
			this.beginUpdate();
			try
			{
				subFragment.beginUpdate();
				try
				{
					if (cascade && subFragment.unmarshalAllSubFragments)
						subFragment.unmarshalAllSubFragments(cascade);

					var ctab = this.getCtab();
					/*
					for (var i = 0, l = subFragment.getNodeCount(); i < l; ++i)
						ctab.appendNode(subFragment.getNodeAt(i));
					for (var i = 0, l = subFragment.getConnectorCount(); i < l; ++i)
						ctab.appendConnector(subFragment.getConnectorAt(i));
					*/
					Kekule.StructureFragment.moveChildBetweenStructFragment(subFragment, this,
						subFragment.getNodes(), subFragment.getConnectors(), true);  // ignore anchor nodes
					subFragment.clear();
					ctab.removeNode(subFragment);
				}
				finally
				{
					subFragment.endUpdate();
					subFragment.finalize();
				}
			}
			finally
			{
				this.endUpdate();
			}
		}
	},
	/**
	 * Remove all sub {@link Kekule.StructureFragment} and move their nodes and connectors into this fragment.
	 * @param {Bool} cascade If subfragments should also unmarshal their children fragments.
	 */
	unmarshalAllSubFragments: function(cascade)
	{
		var sfs = this.getSubFragments();
		if (sfs)
		{
			for (var i = 0, l = sfs.length; i < l; ++i)
			{
				/*
				if (cascade && (sfs[i].unmarshalSubFragment))
					sfs[i].unmarshalSubFragment(cascade);
				*/
				this.unmarshalSubFragment(sfs[i], cascade);
			}
		}
	},

	/**
	 * Recalculate coords of group and child notes.
	 * The coords of child nodes are based on group coord, while group coord is calculated
	 * by anchorNodes. So those value may need to be recalculate when anchor nodes changed.
	 */
	recalcCoords: function()
	{
		if (this.hasCtab())
		{
			this.beginUpdate();
			try
			{
				//var has2D = this.hasCoord2D();
				//var has3D = this.hasCoord3D();
				var newCenter2D = {};  //{'x': 0, 'y': 0};
				var newCenter3D = {};  //{'x': 0, 'y': 0, 'z': 0};
				var anchorCount = this.getAnchorNodeCount();
				for (var i = 0, l = anchorCount; i < l; ++i)
				{
					var node = this.getAnchorNodeAt(i);
					//if (has2D)
						//newCenter2D = Kekule.CoordUtils.add(newCenter2D, node.fetchCoord2D());
					newCenter2D = Kekule.CoordUtils.add(newCenter2D, node.getCoord2D() || {});
					//if (has3D)
						//newCenter3D = Kekule.CoordUtils.add(newCenter3D, node.fetchCoord3D());
					newCenter3D = Kekule.CoordUtils.add(newCenter3D, node.getCoord3D() || {});
				}

				//if (has2D)
				var notUnset = Kekule.ObjUtils.notUnset;
				var has2D = (notUnset(newCenter2D.x) || notUnset(newCenter2D.y));
				var has3D = (notUnset(newCenter3D.x) || notUnset(newCenter3D.y) || notUnset(newCenter3D.z));
				if (has2D)
				{
					var delta2D = Kekule.CoordUtils.substract({}, newCenter2D);
					this.setCoord2D(Kekule.CoordUtils.substract(this.fetchCoord2D(), delta2D));
				}
				if (has3D)
				{
					var delta3D = Kekule.CoordUtils.substract({}, newCenter3D);
					this.setCoord3D(Kekule.CoordUtils.substract(this.fetchCoord3D(), delta3D));
				}

				for (var i = 0, l = this.getNodeCount(); i < l; ++i)
				{
					var node = this.getNodeAt(i);
					if (has2D)
						node.setCoord2D(Kekule.CoordUtils.add(node.fetchCoord2D(), delta2D));
					if (has3D)
						node.setCoord3D(Kekule.CoordUtils.add(node.fetchCoord3D(), delta3D));
				}
			}
			finally
			{
				this.endUpdate();
			}
		}
	},

	/**
	 * Copy additional structure info to shadow fragment.
	 * @param shadowFragment
	 * @param srcToShadowMap
	 * @param shadowToSrcMap
	 * @private
	 */
	_copyAdditionalInfoToShadowFragment: function(shadowFragment, srcToShadowMap, shadowToSrcMap)
	{
		var sourceFragment = this;
		// copy aromatic rings info
		var srcAromaticRings = sourceFragment.getAromaticRings();
		if (srcAromaticRings && srcAromaticRings.length)
		{
			var shadowAromaticRings = [];
			for (var i = 0, l = srcAromaticRings.length; i < l; ++i)
			{
				var srcAromaticRing = srcAromaticRings[i];
				var shadowNodes = [], shadowConnectors = [];
				for (var j = 0, k = srcAromaticRing.nodes.length; j < k; ++j)
				{
					var shadowObj = srcToShadowMap.get(srcAromaticRing.nodes[j]);
					if (shadowObj)
						shadowNodes.push(shadowObj);
				}
				for (var j = 0, k = srcAromaticRing.connectors.length; j < k; ++j)
				{
					var shadowObj = srcToShadowMap.get(srcAromaticRing.connectors[j]);
					if (shadowObj)
						shadowConnectors.push(shadowObj);
				}
				var shadowRing = Object.extend({}, srcAromaticRing);
				shadowRing.nodes = shadowNodes;
				shadowRing.connectors = shadowConnectors;
				//shadowAromaticRings.push({'nodes': shadowNodes, 'connectors': shadowConnectors});
				shadowAromaticRings.push(shadowRing);
			}
			shadowFragment.setAromaticRings(shadowAromaticRings);
		}
	},
	/**
	 * Returns the shadow fragment with all subgroups unmarshalled
	 * @param {Bool} autoCreate Whether allow to create new one when the shadow does not exists.
	 * @returns {Kekule.StructureFragment}
	 */
	getFlattenedShadowFragment: function(autoCreate)
	{
		if (this.getFlattenedShadowOnSelf())
			return this;
		else
		{
			var shadow = this.getFlattenedShadow(autoCreate);
			return shadow ? shadow.getShadowFragment() : null;
		}
	},
	/**
	 * Returns the flatterned shadowed object of source.
	 * @param {Kekule.ChemStructureObject} srcObj
	 * @returns {Kekule.ChemStructureObject}
	 */
	getFlatternedShadowShadowObj: function(srcObj, autoCreate)
	{
		if (this.getFlattenedShadowOnSelf())
			return srcObj;
		else
		{
			var shadow = this.getFlattenedShadow(autoCreate);
			return shadow ? shadow.getShadowObj(srcObj) : null;
		}
	},
	/**
	 * Returns the source object from flatterned shadow.
	 * @param {Kekule.ChemStructureObject} shadowObj
	 * @returns {Kekule.ChemStructureObject}
	 */
	getFlatternedShadowSourceObj: function(shadowObj, autoCreate)
	{
		if (this.getFlattenedShadowOnSelf())
			return shadowObj;
		else
		{
			var shadow = this.getFlattenedShadow(autoCreate);
			return shadow ? shadow.getSourceObj(shadowObj) : null;
		}
	},

	/**
	 * Returns the flatterned shadowed objects of source.
	 * @param {Array} srcObjs
	 * @returns {Array}
	 */
	getFlatternedShadowShadowObjs: function(srcObjs, autoCreate)
	{
		if (this.getFlattenedShadowOnSelf())
			return srcObjs;
		else
		{
			var shadow = this.getFlattenedShadow(autoCreate);
			var result = [];
			if (shadow)
			{
				for (var i = 0, l = srcObjs.length; i < l; ++i)
				{
					result.push(shadow.getShadowObj(srcObjs[i]))
				}
			}
			return result
		}
	},
	/**
	 * Returns the source objects from flatterned shadow.
	 * @param {Array} shadowObjs
	 * @returns {Array}
	 */
	getFlatternedShadowSourceObjs: function(shadowObjs, autoCreate)
	{
		if (this.getFlattenedShadowOnSelf())
			return shadowObjs;
		else
		{
			var shadow = this.getFlattenedShadow(autoCreate);
			var result = [];
			if (shadow)
			{
				for (var i = 0, l = shadowObjs.length; i < l; ++i)
				{
					result.push(shadow.getSourceObj(shadowObjs[i]));
				}
			}
			return result;
		}
	},

	/**
	 * Create a shadow fragment of this one.
	 * @returns {@link Kekule.StructureFragmentShadow}
	 */
	createShadow: function()
	{
		var result = new Kekule.StructureFragmentShadow(this);
		this._copyAdditionalInfoToShadowFragment(result.getShadowFragment(), result.getSourceToShadowMap(), result.getShadowToSourceMap());
		return result;
	},

	/**
	 * Traverse the nodes in connection tab through a depth or breadth first spanning tree algorithm.
	 * @param {Func} callback Function called when meet a new node or connector, has two params: callback(currNodeOrConnector, isConnector)
	 * @param {Kekule.StructureNode} startingNode Starting position of travers.
	 * @param {Bool} breadthFirst Set to true to use breadth first algorithm or false to use depth first algorithm.
	 * @param {Array} partialNodes If this param is set, only part of the structure will be traversed.
	 * @returns {Hash} A hash object containing all the nodes and connectors sequence traversed. {nodes, connectors}.
	 *   Note that all nodes but not all connectors (e.g., the one in ring) may be traversed.
	 *   If the structure has no ctab, null will be returned.
	 */
	traverse: function(callback, startingNode, breadthFirst, partialNodes)
	{
		if (this.hasCtab())
			return this.getCtab().traverse(callback, startingNode, breadthFirst, partialNodes);
		else
			return null;
	}
});

/**
 * Move nodes and connectors from target to dest structure fragment.
 * @param {Kekule.StructureFragment} target
 * @param {Kekule.StructureFragment} dest
 * @param {Array} moveNodes
 * @param {Array} moveConnectors
 * @param {Bool} ignoreAnchorNodes
 */
Kekule.StructureFragment.moveChildBetweenStructFragment = function(target, dest, moveNodes, moveConnectors, ignoreAnchorNodes)
{
	var CU = Kekule.CoordUtils;

	target.beginUpdate();
	dest.beginUpdate();
	var anchorNodes = target.getAnchorNodes();
	try
	{
		// TODO: here we need change coord if essential
		var targetCoord2D = target.getAbsCoord2D();
		var targetCoord3D = target.getAbsCoord3D();
		var destCoord2D = dest.getAbsCoord2D();
		var destCoord3D = dest.getAbsCoord3D();

		var coordDelta2D = CU.substract(targetCoord2D, destCoord2D);
		var coordDelta3D = CU.substract(targetCoord3D, destCoord3D);

		//console.log('coordDelta', coordDelta2D, coordDelta3D);

		var nodes = Kekule.ArrayUtils.clone(moveNodes);
		var connectors = Kekule.ArrayUtils.clone(moveConnectors);
		for (var i = 0, l = nodes.length; i < l; ++i)
		{
			var node = nodes[i];
			var index = target.indexOfNode(node);
			if (index >= 0)
			{
				target.removeNodeAt(index, true);  // preserve linked connectors

				var oldCoord2D = node.getCoord2D();
				if (oldCoord2D)
				{
					var newCoord2D = CU.add(oldCoord2D, coordDelta2D);
					node.setCoord2D(newCoord2D);
				}
				var oldCoord3D = node.getCoord3D();
				if (oldCoord3D)
				{
					var newCoord3D = CU.add(oldCoord3D, coordDelta3D);
					node.setCoord2D(newCoord3D);
				}

				dest.appendNode(node);
				if (anchorNodes.indexOf(node)>= 0)
				{
					target.removeAnchorNode(node);
					if (!ignoreAnchorNodes)
						dest.appendAnchorNode(node);
				}
			}
		}
		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			var connector = connectors[i];
			var index = target.indexOfConnector(connector);
			if (index >= 0)
			{
				target.removeConnectorAt(index, true);  // preserve linked objects
				dest.appendConnector(connector);
			}
		}
	}
	finally
	{
		//console.log('[struct merge done]');
		dest.endUpdate();
		target.endUpdate();
	}
};

/**
 * Represent an "shadow" of structure fragment.
 * The shadow is cloned fragment from source with two maps (source->shadow and shadow->source)
 * to connect all child nodes/connectors between shadow and source fragment.
 * @class
 * @augments ObjectEx
 *
 * @param {Kekule.StructureFragment} srcFragment
 *
 * @property {Kekule.MapEx} shadowToSourceMap
 * @property {Kekule.MapEx} sourceToShadowMap
 * @property {Kekule.StructureFragment} shadowFragment
 */
Kekule.StructureFragmentShadow = Class.create(ObjectEx,
/** @lends Kekule.StructureFragmentShadow# */
{
	/** @private */
	CLASS_NAME: 'Kekule.StructureFragmentShadow',
	initialize: function(/*$super, */srcFragment)
	{
		if (!srcFragment)
		{
			Kekule.error(Kekule.$L('ErrorMsg.SOURCE_FRAGMENT_NOT_SET'));
			return;
		}
		this.tryApplySuper('initialize')  /* $super() */;
		this.setPropStoreFieldValue('shadowToSourceMap', new Kekule.MapEx());
		this.setPropStoreFieldValue('sourceToShadowMap', new Kekule.MapEx());
		var shadowFragment = this._createShadowFragment(srcFragment, this.getSourceToShadowMap(), this.getShadowToSourceMap());
		this.setPropStoreFieldValue('shadowFragment', shadowFragment);
	},
	doFinalize: function(/*$super*/)
	{
		this.getShadowToSourceMap().finalize();
		this.getSourceToShadowMap().finalize();
		this.getShadowFragment().finalize();
		this.setPropStoreFieldValue('shadowToSourceMap', null);
		this.setPropStoreFieldValue('sourceToShadowMap', null);
		this.setPropStoreFieldValue('shadowFragment', null);
		this.tryApplySuper('doFinalize')  /* $super() */;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('shadowFragment', {'dataType': 'Kekule.StructureFragment', 'setter': null});
		this.defineProp('sourceToShadowMap', {'dataType': 'Kekule.MapEx', 'setter': null});
		this.defineProp('shadowToSourceMap', {'dataType': 'Kekule.MapEx', 'setter': null});
	},

	/**
	 * Create the shadow fragment from source, and fill the two direction maps.
	 * @param sourceFragment
	 * @private
	 */
	_createShadowFragment: function(sourceFragment, srcToShadowMap, shadowToSrcMap)
	{
		var result = sourceFragment.clone();
		this._mapFragmentChildren(sourceFragment, result, srcToShadowMap, shadowToSrcMap);
		return result;
	},
	/** @private */
	_mapFragmentChildren: function(src, shadow, srcToShadowMap, shadowToSrcMap)
	{
		for (var i = 0, l = src.getChildCount(); i < l; ++i)
		{
			var srcObj = src.getChildAt(i);
			var shadowObj = shadow.getChildAt(i);
			srcToShadowMap.set(srcObj, shadowObj);
			shadowToSrcMap.set(shadowObj, srcObj);
			// if object is nested, cascade the mapping
			if (srcObj.getChildCount && shadowObj.getChildCount)
			{
				this._mapFragmentChildren(srcObj, shadowObj, srcToShadowMap, shadowToSrcMap);
			}
		}
	},

	/**
	 * Returns the shadowed object of source.
	 * @param {Kekule.ChemStructureObject} srcObj
	 * @returns {Kekule.ChemStructureObject}
	 */
	getShadowObj: function(srcObj)
	{
		return this.getSourceToShadowMap().get(srcObj);
	},
	/**
	 * Returns the source object from shadow.
	 * @param {Kekule.ChemStructureObject} shadowObj
	 * @returns {Kekule.ChemStructureObject}
	 */
	getSourceObj: function(shadowObj)
	{
		return this.getShadowToSourceMap().get(shadowObj);
	}
});

/**
 * Represent an substituent group.
 * @class
 * @augments Kekule.StructureFragment
 * @param {String} id Id of this node.
 * @param {String} abbr Abbreviation of group.
 * @param {String} name Name of group.
 * @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
 * @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
 *
 * @property {String} name Name of group.
 * @property {String} abbr Abbreviation of group, e.g. OMe.
 * @property {String} formulaText Formula in plain text to represent subgroup, e.g. CO2H.
 *   This property has nothing to do with the actual formula of subgroup.
 */
Kekule.SubGroup = Class.create(Kekule.StructureFragment,
/** @lends Kekule.SubGroup# */
{
	/** @private */
	CLASS_NAME: 'Kekule.SubGroup',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, abbr, name, coord2D, coord3D)
	{
		this.tryApplySuper('initialize', [id, coord2D, coord3D])  /* $super(id, coord2D, coord3D) */;
		this.setPropStoreFieldValue('abbr', abbr);
		this.setPropStoreFieldValue('name', name);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('abbr', {'dataType': DataType.STRING});
		this.defineProp('formulaText', {'dataType': DataType.STRING});
		this.defineProp('name', {'dataType': DataType.STRING});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'g';
	},
	/** @private */
	doPropChanged: function(/*$super, */propName, newValue)
	{
		if (propName == 'anchorNodes')  // anchor nodes changed, need to recalculate coords of group and child nodes
		{
			this.recalcCoords();
		}
		return this.tryApplySuper('doPropChanged', [propName, newValue])  /* $super(propName, newValue) */;
	},
	/** @ignore */
	getLabel: function()
	{
		return this.getName() || this.getAbbr() || this.getFormulaText() || Kekule.ChemStructureNodeLabels.SUBGROUP;
	}
});
// RGroup is often used in organic chemistry, here we define it as an alias of SubGroup
Kekule.RGroup = Kekule.SubGroup;

/**
 * Represent an molecule.
 * @class
 * @augments Kekule.StructureFragment
 * @param {String} id Id of this molecule.
 * @param {String} name Name of molecule.
 * @param {Bool} withCtab Create a new molecule with a new ctab.
 *
 * @property {String} name Name of molecule.
 */
Kekule.Molecule = Class.create(Kekule.StructureFragment,
/** @lends Kekule.Molecule# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Molecule',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, name, withCtab)
	{
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
		this.setPropStoreFieldValue('name', name);
		if (withCtab)
			this.setCtab(new Kekule.StructureConnectionTable());
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('name', {'dataType': DataType.STRING});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'm';
	}
});


//=========================================================

/**
 * Implements the concept of a connections between two or more structure nodes.
 * @class
 * @augments Kekule.ChemStructureObject
 * @param {String} id Id of this connector.
 * @param {Array} connectedObjs Objects ({@link Kekule.ChemStructureObject}) connected by connected, usually a connector connects two nodes.
 *
 * @property {Array} connectedObjs Structure objects ({@link Kekule.ChemStructureObject}) connected by connector.
 *   Usually a connector connects two nodes. However, there are some compounds that has bond-atom bond
 *   (such as Zeise's salt: [Cl3Pt(CH2=CH2)]), so here array of {@link Kekule.ChemStructureObject}
 *   rather than array of {@link Kekule.ChemStructureNode} is used.
 * @property {Array} neighboringConnectors Readonly, all connectors which has the one of the same connectedObjs of this one.
 */
Kekule.BaseStructureConnector = Class.create(Kekule.ChemStructureObject,
/** @lends Kekule.BaseStructureConnector# */
{
	/** @private */
	CLASS_NAME: 'Kekule.BaseStructureConnector',
	/** @constructs */
	initialize: function(/*$super, */id, connectedObjs)
	{
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
		//this.setPropStoreFieldValue('connectedObjs', connectedObjs || []);
		this.setConnectedObjs(connectedObjs || []);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('connectedObjs', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLISHED,
			'setter': function(value)
				{
					var objs = this.getPropStoreFieldValue('connectedObjs');
					if (!objs)
					{
						this.setPropStoreFieldValue('connectedObjs', []);
						objs = this.getPropStoreFieldValue('connectedObjs');
					}
					// remove linkedConnectors in all existed connectedObjs
					for (var i = 0, l = objs.length; i < l; ++i)
					{
						objs[i].removeLinkedConnector(this);
					}

					if (value)
					{
						// assert all items in value can be linked in
						for (var i = 0, l = value.length; i < l; ++i)
							this.assertConnectedObjLegal(value[i]);
						for (var i = 0, l = value.length; i < l; ++i)
						{
							var obj = value[i];
							var actualConnObj = obj.getCurrConnectableObj? obj.getCurrConnectableObj(): obj;
							Kekule.ArrayUtils.pushUnique(objs, actualConnObj);
						}
					}
					else
						this.setPropStoreFieldValue('connectedObjs', []);

					// add new linkedConnectors in new connectedObjs
					objs = this.getPropStoreFieldValue('connectedObjs');
					for (var i = 0, l = objs.length; i < l; ++i)
					{
						objs[i].appendLinkedConnector(this);
					}

					this.notifyConnectedObjsChanged();
				}
		});
		this.defineProp('neighboringConnectors', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null,
			'getter': function()
			{
				var result = [];
				var connObjs = this.getConnectedObjs();
				for (var i = 0, l = connObjs.length; i < l; ++i)
				{
					var connObj = connObjs[i];
					if (connObj.getLinkedConnectors)
						Kekule.ArrayUtils.pushUnique(result, connObj.getLinkedConnectors() || []);
				}
				// remove self
				Kekule.ArrayUtils.remove(result, this);
				return result;
			}
		});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'c';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['connectedObjs']);
	},

	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'connectedObjCount'))
			{
				var c1 = this.getConnectedObjCount();
				var c2 = targetObj.getConnectedObjCount && targetObj.getConnectedObjCount();
				result = this.doCompareOnValue(c1, c2, options);
			}
		}
		return result;
	},

	/**
	 * Notify {@link Kekule.ChemStructureConnector#connectedObjs} property has been changed
	 * @private
	 */
	notifyConnectedObjsChanged: function()
	{
		this.notifyPropSet('connectedObjs', this.getPropStoreFieldValue('connectedObjs'));
	},

	/**
	 * Check if an obj can be added to connectedObjs array.
	 * Generally, an object with same owner of current connector can be added. If not so,
	 * a exception will be raised.
	 * @param {Kekule.ChemStructureObject} obj
	 * @returns {Bool}
	 */
	assertConnectedObjLegal: function(obj)
	{
		if ((!obj) || (!obj.getOwner))
		{
			Kekule.chemError(
				Kekule.hasLocalRes()?
					/*Kekule.ErrorMsg.UNABLE_ADD_MISTYPED_NODE*/Kekule.$L('ErrorMsg.UNABLE_ADD_MISTYPED_NODE') :
					'Unable to link mistyped node to connector'
			);
		}
		else if (obj.getOwner() && obj.getOwner() != this.getOwner())
		// if owner is null, still allow connect, this may occur when undo remove a subgroup in editor
		{
			Kekule.chemError(
				Kekule.hasLocalRes()?
					/*Kekule.ErrorMsg.UNABLE_ADD_DIFF_OWNER_OBJ*/Kekule.$L('ErrorMsg.UNABLE_ADD_DIFF_OWNER_OBJ') :
					'Object with different owner can not be linked to connector'
			);
			return false;
		}
		else
			return true;
	},
	/**
	 * Called when a connected object is added to list.
	 * @private
	 */
	connectedObjAdded: function(obj)
	{
		this.notifyConnectedObjsChanged();
	},
	/**
	 * Get count of connected objects.
	 * @returns {Int}
	 */
	getConnectedObjCount: function()
	{
		return this.getConnectedObjs().length;
	},
	/**
	 * Get index of obj in connectedObjs array.
	 * @param {Kekule.ChemStructureObject} obj
	 * @returns {Int}
	 */
	indexOfConnectedObj: function(obj)
	{
		return this.getConnectedObjs().indexOf(obj);
	},
	/**
	 * Get connectedObj at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemStructureObject}
	 */
	getConnectedObjAt: function(index)
	{
		return this.getConnectedObjs()[index];
	},
	/**
	 * Set connectedObj at index.
	 * @param {Int} index
	 * @param {Kekule.ChemStructureObject} value
	 */
	setConnectedObjAt: function(index, value)
	{
		this.assertConnectedObjLegal(value);
		var node = this.getConnectedObjs()[index];
		if (node)
			node.removeLinkedConnector(this);
		// in node.removeLinkedConnector, this.connectedObjs already changes and count declines 1
		// do should not change array with index directly but do a insert
		this.insertConnectedObjAt(value, index);
		//this.getConnectedObjs()[index] = value;
		this.connectedObjAdded(value);
	},
	/**
	 * Add a object to connectedObjs array. If obj already in connectedObjs, nothing will be done.
	 * @param {Kekule.ChemStructureObject} obj
	 */
	appendConnectedObj: function(obj)
	{
		this.assertConnectedObjLegal(obj);
		var actualConnObj = obj.getCurrConnectableObj? obj.getCurrConnectableObj(): obj;
		var result = this._doAppendConnectedObj(actualConnObj);
		if (actualConnObj)
			actualConnObj.appendLinkedConnector(this);
		return result;
	},
	/** @private */
	_doAppendConnectedObj: function(obj)
	{
		var r = Kekule.ArrayUtils.pushUniqueEx(this.getConnectedObjs(), obj);
		if (r.isPushed)
			this.connectedObjAdded(obj);
		return r.index;
	},
	/**
	 * Insert an object to connectedObjs array before refSibling. If refSibling is not set, obj will be push to the tail of connectedObjs.
	 * @param {Kekule.ChemStructureObject} obj
	 * @param {Kekule.ChemStructureObject} refSibling
	 * @returns {Int} Index of newly inserted object.
	 */
	insertConnectedObjBefore: function(obj, refSibling)
	{
		this.assertConnectedObjLegal(obj);
		var actualRefObj = refSibling? (refSibling.getCurrConnectableObj? refSibling.getCurrConnectableObj(): refSibling): null;
		var refIndex = actualRefObj? this.indexOfConnectedObj(actualRefObj): -1;
		if (refIndex < 0)
			return this.appendConnectedObj(obj);
		else
			return this.insertConnectedObjAt(obj, refIndex);
	},
	/**
	 * Insert obj at index of connectedObjs array. If index is not set, obj will be put as the first obj.
	 * @param {Kekule.ChemStructureObject} obj
	 * @param {Int} index
	 */
	insertConnectedObjAt: function(obj, index)
	{
		if (!index)
			index = 0;
		this.assertConnectedObjLegal(obj);
		var actualConnObj = obj.getCurrConnectableObj? obj.getCurrConnectableObj(): obj;
		var i = this.indexOfConnectedObj(actualConnObj);
		var objs = this.getConnectedObjs();
		if (i >= 0)  // already inside, adjust position
		{
			objs.splice(i, 1);
			objs.splice(index, 0, actualConnObj);
			this.notifyConnectedObjsChanged();
		}
		else // new one
		{
			objs.splice(index, 0, actualConnObj);
			if (actualConnObj)
				actualConnObj.appendLinkedConnector(this);
			//console.log('insert new one', obj.getLinkedConnectorCount());
			this.connectedObjAdded(actualConnObj);
		}
	},
	/**
	 * Remove object at index in connectedObjs property.
	 * @param {Int} index
	 */
	removeConnectedObjAt: function(index)
	{
		var node = this.getConnectedObjs()[index];
		if (node)
		{
			//this.getConnectedObjs().removeAt(index);
			//this.getConnectedObjs().splice(index, 1);
			this._doRemoveConnectedObjAt(index);
			//node.removeLinkedConnector(this);
			node._doRemoveLinkedConnectorAt(node.indexOfLinkedConnector(this));
			this.notifyConnectedObjsChanged();
		}
	},
	/** @private */
	_doRemoveConnectedObjAt: function(index)
	{
		this.getConnectedObjs().splice(index, 1);
	},
	/**
	 * Remove a object in connectedObjs property.
	 * @param {Kekule.ChemStructureObject} obj
	 */
	removeConnectedObj: function(obj)
	{
		var index = this.getConnectedObjs().indexOf(obj);
		if (index >= 0)
			this.removeConnectedObjAt(index);
	},
	/**
	 * Replace old connected object with new one and remove connection to old object.
	 * @param {Kekule.ChemStructureObject} oldObj
	 * @param {Kekule.ChemStructureObject} newObj
	 */
	replaceConnectedObj: function(oldObj, newObj)
	{
		var index = this.indexOfConnectedObj(oldObj);
		if (index >= 0)
			this.setConnectedObjAt(index, newObj);
	},
	/**
	 * Clear all connected objects.
	 */
	clearConnectedObjs: function()
	{
		this.setConnectedObjs([]);
	},
	/**
	 * Check if a object is connected with this connector.
	 * @param {Kekule.ChemStructureObject} obj
	 */
	hasConnectedObj: function(obj)
	{
		return (this.getConnectedObjs().indexOf(obj) >= 0);
	},
	/**
	 * Check if a object is connected with this connector.
	 * Same as {@link Kekule.ChemStructureConnector#hasConnectedObj}
	 * @param {Kekule.ChemStructureObject} obj
	 */
	isConnectingWithObj: function(obj)
	{
		return this.hasConnectedObj(obj);
	},
	/**
	 * Sort the array of connected objs.
	 * @param {Function} compareFunc
	 */
	sortConnectedObjs: function(compareFunc)
	{
		this.getConnectedObjs().sort(compareFunc);
	},
	/**
	 * Reverse the order of connected object.
	 */
	reverseConnectedObjOrder: function()
	{
		this.setPropStoreFieldValue('connectedObjs', Kekule.ArrayUtils.reverse(this.getConnectedObjs()));
		this.notifyConnectedObjsChanged();
	},

	/**
	 * Remove this connector from all linked objects.
	 * Ths method should be called before a connector is removed from a structure.
	 */
	removeThisFromConnectedObjs: function()
	{
		for (var i = this.getConnectedObjCount() - 1; i >= 0; --i)
		{
			var o = this.getConnectedObjAt(i);
			o.removeLinkedConnector(this);
		}
	},

	/**
	 * Returns connected objects with the same parent of this connector.
	 * For example, connector connected to a child node in subgroup, then the subgroup
	 * rather than the child node will be returned.
	 * @returns {Array}
	 */
	getConnectedSiblings: function()
	{
		var objs = this.getConnectedObjs();
		var result = [];
		var parent = this.getParent();

		/** @ignore */
		var getSiblingParent = function(childObj)
		{
			if (childObj.getParent)
			{
				var p = childObj.getParent();
				if (p === parent)
					return p;
				else
					return getSiblingParent(p);
			}
			else
				return childObj;
		};

		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (!obj.getParent || (obj.getParent() === parent))
				result.push(obj);
			else
			{
				var o = getSiblingParent(obj);
				if (o)
					result.push(o);
			}
		}

		return result;
	},

	/**
	 * Check if this connector connected to another connector (e.g, bond ends with another bond).
	 */
	isConnectingConnector: function()
	{
		for (var i = 0, l = this.getConnectedObjCount(); i < l; ++i)
		{
			var o = this.getConnectedObjAt(i);
			if (o instanceof Kekule.ChemStructureConnector)
				return true;
		}
		return false;
	}
});

/**
 * Implements the concept of a connections between two or more structure nodes.
 * @class
 * @augments Kekule.BaseStructureConnector
 *
 * @property {Array} connectedChemNodes Nodes connected with this connector.
 * @property {Int} parity Stereo parity of connector.
 */
Kekule.ChemStructureConnector = Class.create(Kekule.BaseStructureConnector,
/** @lends Kekule.ChemStructureConnector# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructureConnector',
	/** @private */
	initProperties: function()
	{
		this.defineProp('parity', {'dataType': DataType.INT});
		this.defineProp('connectedChemNodes', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null,
			'getter': function()
			{
				var result = [];
				for (var i = 0, l = this.getConnectedObjCount(); i < l; ++i)
				{
					var obj = this.getConnectedObjAt(i);
					if (obj instanceof Kekule.ChemStructureNode)
						result.push(obj);
				}
				return result;
			}
		});
	},

	/** @private */
	getAcceptCoordStickFrom: function(fromObj)
	{
		return (!this.isSiblingWith(fromObj) && !(fromObj instanceof Kekule.ChemStructureNode) && !(fromObj instanceof Kekule.BaseStructureConnector));
	},

	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'stereo'))  // parity null/0 should be regard as one in comparison
			{
				var c1 = this.getParity() || Kekule.StereoParity.UNKNOWN;
				var c2 = (targetObj.getParity && targetObj.getParity()) || Kekule.StereoParity.UNKNOWN;
				result = this.doCompareOnValue(c1, c2, options);
			}
		}
		return result;
	},

	/** @ignore */
	clearStructureFlags: function()
	{
		this.setParity(Kekule.StereoParity.NONE);
	},

	/**
	 * Get count of connected chem nodes.
	 * @returns {Int}
	 */
	getConnectedChemNodeCount: function()
	{
		return this.getConnectedChemNodes().length;
	},
	/**
	 * Returns connected objects except hydrogen atoms.
	 * @returns {Array}
	 */
	getConnectedNonHydrogenObjs: function()
	{
		var result = [];
		var objs = this.getConnectedObjs();
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (!obj.isHydrogenAtom || !obj.isHydrogenAtom())
				result.push(obj);
		}
		return result;
	},
	/**
	 * Whether this connector connect hydrogen atom to another node.
	 * @returns {Bool}
	 */
	isNormalConnectorToHydrogen: function()
	{
		return this.getConnectedNonHydrogenObjs().length <= 1;
	}
});

/**
 * Enumeration of possible stereo types of two-atom bonds. The
 * Stereo type defines not just define the stereochemistry, but also the
 * which atom is the stereo center for which the Stereo is defined.
 * The first atom in the bond (index = 0) is the start atom, while
 * the second atom (index = 1) is the end atom.
 * @class
 */
Kekule.BondStereo = {
	/** A bond for which there is no stereochemistry. */
	NONE: 0,
	/** A bond pointing up of which the start atom is the stereocenter and
	 * the end atom is above the drawing plane. */
	UP: 1,
	/** A bond pointing up of which the end atom is the stereocenter and
	 * the start atom is above the drawing plane. */
	UP_INVERTED: 2,
	/** A bond pointing down of which the start atom is the stereocenter
	 * and the end atom is below the drawing plane. */
	DOWN: 3,
	/** A bond pointing down of which the end atom is the stereocenter and
	 * the start atom is below the drawing plane. */
	DOWN_INVERTED: 4,
	/** A bond for which there is stereochemistry, we just do not know
	 *  if it is UP or DOWN. The start atom is the stereocenter.
	 */
	UP_OR_DOWN: 8,
  /** A bond for which there is stereochemistry, we just do not know
   *  if it is UP or DOWN. The end atom is the stereocenter.
   */
  UP_OR_DOWN_INVERTED: 9,
	/** A bond is closer to observer than papaer, often used in ring structures. */
	CLOSER: 10,
	/** Indication that this double bond has a fixed, but unknown E/Z
	 * configuration.
	 */
	E_OR_Z: 20,
  /** Indication that this double bond has a E configuration.
   */
  E: 21,
  /** Indication that this double bond has a Z configuration.
   */
  Z: 22,
	/** Indication that this double bond has a fixed configuration, defined
	 * by the 2D and/or 3D coordinates.
	 */
	E_Z_BY_COORDINATES: 23,
	/** Indication that this double bond has a fixed, but unknown cis/trans
	 * configuration.
	 */
	CIS_OR_TRANS: 30,
	/** Indication that this double bond has a Cis configuration.
   */
	CIS: 31,
	/** Indication that this double bond has a Trans configuration.
   */
	TRANS: 32,

	/**
	 * Get inverted stereo direction value.
	 * @param {Int} direction
	 * @returns {Int}
	 */
	getInvertedDirection: function(direction)
	{
		var S = Kekule.BondStereo;
		switch (direction)
		{
			case S.UP: return S.UP_INVERTED;
			case S.UP_INVERTED: return S.UP;
			case S.DOWN: return S.DOWN_INVERTED;
			case S.DOWN_INVERTED: return S.DOWN;
			case S.UP_OR_DOWN: return S.UP_OR_DOWN_INVERTED;
			default:
				return direction;
		}
	}
};

/**
 * Implements the concept of a covalent bond between two or more atoms. A bond is
 * considered to be a number of electrons connecting two or more of atoms.
 * @class
 * @augments Kekule.ChemStructureConnector
 * @param {String} id Id of this node.
 * @param {Array} connectedObjs Objects connected by connector, usually a connector connects two nodes.
 * @param {Int} bondOrder Order of bond. Usually electronCount / 2.
 * @param {Float} electronCount Count of electrons in this set.
 * @param {String} bondType Type of bond, value from {@link Kekule.BondType}.
 *
 * @property {Kekule.BondForm} bondForm Form of bond, single, double or other.
 * @property {String} bondType Type of bond, value from {@link Kekule.BondType}.
 * @property {Num} bondOrder Order of bond. Values should be retrieved from {@link Kekule.BondOrder}.
 * @property {Num} bondValence Valence comsumed of an atom to connect to this bond. Note this value is different from {@link Kekule.Bond#bondOrder},
 *   For example, bondOrder value for {@link Kekule.BondOrder.EXPLICIT_AROMATIC} is 10, but the valence is 1.5. This property is read only.
 * @property {Float} electronCount Count of electrons in this set.
 *   Note that there may be partial electron in set, so a float value is used here.
 * @property {Int} stereo Stereo type of bond.
 * @property {Bool} isInAromaticRing A flag to indicate the bond is in a aromatic ring and is a aromatic bond.
 *   User should not set this property directly, instead, this value will be marked
 *   in aromatic detection routine.
 * @property {Array} neighboringBonds Readonly, all neighboring bonds that has at least one connected object of this one.
 */
Kekule.Bond = Class.create(Kekule.ChemStructureConnector,
/** @lends Kekule.Bond# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Bond',
	/** @constructs */
	initialize: function(/*$super, */id, connectedObjs, bondOrder, electronCount, bondType)
	{
		this.tryApplySuper('initialize', [id, connectedObjs || []])  /* $super(id, connectedObjs || []) */;
		if (bondOrder || electronCount || bondType)
			this.setBondForm(Kekule.BondFormFactory.getBondForm(bondOrder, electronCount, bondType));
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('bondForm', {'dataType': 'Kekule.BondForm', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC});
		this.defineProp('bondType', {
			'dataType': DataType.STRING,
			'enumSource': Kekule.BondType,
			'getter': function()
				{
					var f = this.getBondForm();
					return f? f.getBondType(): Kekule.BondType.DEFAULT;
				},
			'setter': function(value)
				{
					this.changeBondForm(this.getBondOrder(), Kekule.ElectronSet.UNSET_ELECTRONCOUNT, value);
				}
		});
		this.defineProp('bondOrder', {
			'dataType': DataType.INT,
			'enumSource': Kekule.BondOrder,
			'getter': function()
				{
					var f = this.getBondForm();
					return f? f.getBondOrder(): Kekule.BondOrder.UNSET;
				},
			'setter': function(value)
				{
					this.changeBondForm(value, Kekule.ElectronSet.UNSET_ELECTRONCOUNT, this.getBondType());
				}
		});
		this.defineProp('bondValence', {
			'dataType': DataType.INT,
			'serializable': false,
			'getter': function()
				{
					var f = this.getBondForm();
					return f? f.getBondValence(): 0;
				},
			'setter': null
		});
		this.defineProp('electronCount', {
			'dataType': DataType.FLOAT,
			'getter': function()
				{
					if (this.isAromatic())
						return 3;
					var f = this.getBondForm();
					return f? f.getElectronCount(): Kekule.ElectronSet.UNSET_ELECTRONCOUNT;
				},
			'setter': function(value)
				{
					var order = this.getBondOrder();
					this.changeBondForm(order, value, this.getBondType());
				}
		});
		this.defineProp('stereo', {'dataType': DataType.INT, 'enumSource': Kekule.BondStereo, 'defaultValue': Kekule.BondStereo.NONE});
		this.defineProp('isInAromaticRing', {'dataType': DataType.BOOL,
			'setter': null,
			'getter': function()
			{
				var result = false;
				var parent = this.getParent();
				if (parent && parent.isConnectorInAromaticRing)
					return parent.isConnectorInAromaticRing(this);
				else
					return false;
			}
		});
		this.defineProp('neighboringBonds', {
			'dataType': DataType.ARRAY,
			'serializable': false,
			'scope': Class.PropertyScope.PUBLIC,
			'setter': null,
			'getter': function()
			{
				var neighboringConnectors = this.getNeighboringConnectors() || [];
				var result = [];
				for (var i = 0, l = neighboringConnectors.length; i < l; ++i)
				{
					if (neighboringConnectors[i] instanceof Kekule.Bond)
						result.push(neighboringConnectors[i]);
				}
				return result;
			}
		});
	},
	/** @private */
	initPropValues: function(/*$super*/)
	{
		this.tryApplySuper('initPropValues')  /* $super() */;
		this.setPropStoreFieldValue('stereo', Kekule.BondStereo.NONE);
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'b';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['bondForm', 'stereo']);
	},
	/** @ignore */
	clearStructureFlags: function()
	{
		this.setPropStoreFieldValue('isInAromaticRing', null);
	},
	/** @ignore */
	doGetParity: function(/*$super*/)  // override parity getter, only double bond can have parity value
	{
		if (this.isDoubleBond())
			return this.tryApplySuper('doGetParity')  /* $super() */;
		else
			return null;
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'bondType'))
				result.push('bondType');
			/* Bond order must handle seperatorly, as there may be aromatic bond
			if (this._getComparisonOptionFlagValue(options, 'bondOrder'))
				result.push('bondOrder');
			*/
		}
		return result;
	},
	/** @ignore */
	doCompare: function(/*$super, */targetObj, options)
	{
		var result = this.tryApplySuper('doCompare', [targetObj, options])  /* $super(targetObj, options) */;
		if (!result && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			if (this._getComparisonOptionFlagValue(options, 'connectedObjCount'))
			{
				var c1 = this.getConnectedObjCount();
				var c2 = targetObj.getConnectedObjCount && targetObj.getConnectedObjCount();
				result = this.doCompareOnValue(c1, c2, options);
			}
			if (!result && this._getComparisonOptionFlagValue(options, 'bondOrder'))
			{
				var eCount1 = Math.round(this.getElectronCount());
				var eCount2 = Math.round(targetObj.getElectronCount());
				result = this.doCompareOnValue(eCount1, eCount2, options);
			}
		}
		return result;
	},

	/**
	 * Change bond form to new order or electron number.
	 * @private
	 */
	changeBondForm: function(bondOrder, electronCount, bondType)
	{
		this.setBondForm(Kekule.BondFormFactory.getBondForm(bondOrder, electronCount, bondType));
	},
	/** @private */
	canInvertBondDirection: function()
	{
		var BT = Kekule.BondType;
		var bType = this.getBondType();
		return [BT.COVALENT, BT.IONIC, BT.METALLIC, BT.HYDROGEN, BT.UNKNOWN].indexOf(bType) >= 0;
	},
	/**
	 * Change bond direction to a inverted one (in case when connected object order swapped).
	 * @private
	 */
	invertBondDirection: function()
	{
		this.setStereo(Kekule.BondStereo.getInvertedDirection(this.getStereo()));
	},

	/** @ignore */
	reverseConnectedObjOrder: function(/*$super*/)
	{
		if (this.canInvertBondDirection())
		{
			this.tryApplySuper('reverseConnectedObjOrder')  /* $super() */;
			// direction of bond also need to be reversed
			this.invertBondDirection();
		}
		else // can not reverse a directed bond, do nothing
		{

		}
	},

	/**
	 * Turn bond direction to a normal up or down one (not up_inverted or down_inverted).
	 */
	normalizeDirection: function()
	{
		var S = Kekule.BondStereo;
		var inverted = [S.UP_INVERTED, S.DOWN_INVERTED, S.UP_OR_DOWN_INVERTED];
		var d = this.getBondStereo();
		if (inverted.indexOf(d) >= 0)
			this.reverseConnectedObjOrder();
	},

	/**
	 * Returns if bond is a covalence bond.
	 * @returns {Bool}
	 */
	isCovalentBond: function()
	{
		return (this.getBondType() === Kekule.BondType.COVALENT);
	},
	/**
	 * Returns if bond is a single covalence bond.
	 * @returns {Bool}
	 */
	isSingleBond: function()
	{
		return (this.getBondType() === Kekule.BondType.COVALENT) && (this.getBondOrder() === Kekule.BondOrder.SINGLE)
			&& (!this.getIsInAromaticRing());
	},
	/**
	 * Returns if bond is a double covalence bond.
	 * @returns {Bool}
	 */
	isDoubleBond: function()
	{
		return (this.getBondType() === Kekule.BondType.COVALENT) && (this.getBondOrder() === Kekule.BondOrder.DOUBLE)
			&& (!this.getIsInAromaticRing());
	},
	/**
	 * Returns if bond is a triple covalence bond.
	 * @returns {Bool}
	 */
	isTripleBond: function()
	{
		return (this.getBondType() === Kekule.BondType.COVALENT) && (this.getBondOrder() === Kekule.BondOrder.TRIPLE);
	},

	/**
	 * Returns if bond is a aromatic one.
	 * @returns {Bool}
	 */
	isAromatic: function()
	{
		return (!!this.getIsInAromaticRing() || (this.getBondOrder() === Kekule.BondOrder.EXPLICIT_AROMATIC))
			&& (this.getBondType() === Kekule.BondType.COVALENT);
	},

	/**
	 * Check if this bond is between two specified atoms.
	 * @param {Variant} atomicNumberOrSymbol1
	 * @param {Variant} atomicNumberOrSymbol2
	 * @returns {Bool}
	 */
	isBondBetween: function(atomicNumberOrSymbol1, atomicNumberOrSymbol2)
	{
		var objs = this.getConnectedObjs();
		if (objs.length !== 2)
			return false;
		var found1, found2;
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var obj = objs[i];
			if (obj instanceof Kekule.Atom)
			{
				if (obj.isElement(atomicNumberOrSymbol1))
					found1 = true;
				else if (obj.isElement(atomicNumberOrSymbol2))
					found2 = true;

				if (found1 && found2)
					return true;
			}
		}
		return false;
	}
});

/**
 * A group of structure object. Each items in the group is in attrib=>obj form.
 * @class
 * @augments Kekule.ChemStructureObject
 * @param {String} id Id of this group.
 *
 * @property {Array} items Items in this group. Each item is a hash: {obj, amount, role...}
 *   where obj is the real ChemStructureObject and the rests are related attributes.
 * @property {Bool} raiseExceptionOnTypeMismatch Whether raise exception when wrong type
 *   of object is added to group.
 */
Kekule.ChemStructureObjectGroup = Class.create(Kekule.ChemStructureObject,
/** @lends Kekule.ChemStructureObjectGroup# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructureObjectGroup',
	/** @private */
	ITEM_BASE_CLASSES: ['Kekule.ChemStructureObject', 'Kekule.ChemStructureObjectGroup'], // the allowed base class of each items in group
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id)
	{
		this.tryApplySuper('initialize', [id])  /* $super(id) */;
		this.setPropStoreFieldValue('items', []);
		this.setPropStoreFieldValue('raiseExceptionOnTypeMismatch', []);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('items', {'dataType': DataType.ARRAY});
		this.defineProp('raiseExceptionOnTypeMismatch', {'dataType': DataType.BOOL, 'defaultValue': true, 'scope': Class.PropertyScope.PUBLIC});
	},
	/** @private */
	getAutoIdPrefix: function()
	{
		return 'sg';
	},
	/** @ignore */
	getStructureRelatedPropNames: function(/*$super*/)
	{
		return this.tryApplySuper('getStructureRelatedPropNames')  /* $super() */.concat(['items']);
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			result.push('items');
		}
		return result;
	},

	/** @private */
	ownerChanged: function(/*$super, */newOwner, oldOwner)
	{
		var items = this.getItems();
		for (var i = 0, l = items.length; i < l; ++i)
		{
			var obj = items[i].obj;
			if (obj && obj.setOwner)
				obj.setOwner(newOwner);
		}
		this.tryApplySuper('ownerChanged', [newOwner, oldOwner])  /* $super(newOwner, oldOwner) */;
	},

	/** @private */
	_removeChildObj: function(/*$super, */obj)
	{
		var index = this.indexOfObj(obj);
		if (index < 0)
			index = this.indexOfItem(obj);
		if (index >= 0)
		{
			this.removeObjAt(index);
		}
		this.tryApplySuper('_removeChildObj', [obj])  /* $super(obj) */;
	},

	/** @private */
	getObjectBaseClasses: function()
	{
		return this.getPrototype().ITEM_BASE_CLASSES;
	},
	/**
	 * Ensure item added to group is really an instance of ITEM_BASE_CLASS
	 * @private
	 */
	ensureItemClass: function(obj)
	{
		var classes = this.getObjectBaseClasses();
		if (classes.length > 0)
		{
			for (var i = 0, l = classes.length; i < l; ++i)
			{
				if (obj instanceof ClassEx.findClass(classes[i]))
					return true;
			}
			// not match
			if (this.getRaiseExceptionOnTypeMismatch())
			{
				var msg =
					Kekule.hasLocalRes()?
						/*Kekule.ErrorMsg.CHEMSTRUCTUREOBJECTGROUP_ITEMCLASS_MISMATCH*/Kekule.$L('ErrorMsg.CHEMSTRUCTUREOBJECTGROUP_ITEMCLASS_MISMATCH') :
						'Mismatched group item class';
				Kekule.raise(msg);
			}
			return false;
		}
		else  // not specified base classes, every object is allowed
			return true;
	},

	/** Notify {@link Kekule.ChemStructureObjectGroup#objs} property has been changed */
	notifyItemsChanged: function()
	{
		this.notifyPropSet('items', this.getPropStoreFieldValue('items'));
	},

	/**
	 * Get index of item in items array.
	 * @param {Object} item
	 * @returns {Int} Index of item in array. If not found, returns -1.
	 */
	indexOfItem: function(item)
	{
		return this.getItems().indexOf(item);
	},
	/**
	 * Get index of obj in items array.
	 * @param {Object} obj
	 * @returns {Int} Index of object in array. If not found, returns -1.
	 */
	indexOfObj: function(obj)
	{
		var items = this.getItems();
		for (var i = 0, l = items.length; i < l; ++i)
		{
			if (items[i].obj == obj)
				return i;
		}
		return -1;
	},
	/**
	 * Check if obj exists in items.
	 * @param {Object} obj
	 * @returns {Bool}
	 */
	hasObj: function(obj)
	{
		return this.indexOfObj(obj) >= 0;
	},
	/**
	 * Set additional fields of item at index.
	 * @param {Object} index
	 * @param {Object} attributes
	 */
	setAttributesAt: function(index, attributes)
	{
		var item = this.getItems()[index];
		if (item)
		{
			var keys = Kekule.ObjUtils.getOwnedFieldNames(attributes);
			for (var i = 0, l = keys.length; i < l; ++i)
			{
				item[keys[i]] = attributes[keys[i]];
			}
		}
	},
	/**
	 * Get count of items in group.
	 * @returns {Int}
	 */
	getItemCount: function()
	{
		return this.getItems().length;
	},
	/**
	 * Get item at index of items array.
	 * @param {Int} index
	 * @return {Object}
	 */
	getItemAt: function(index)
	{
		return this.getItems()[index];
	},
	/**
	 * Append an attrib-object pair item in group.
	 * @param {Hash} item
	 */
	appendItem: function(item)
	{
		this.getItems().push(item);
		return this;
	},
	/**
	 * Insert an attrib-object pair item in group at index.
	 * @param {Hash} item
	 * @param {Int} index
	 */
	insertItem: function(item, index)
	{
		return this.insertItemAt(item, index);
	},
	/**
	 * Insert an attrib-object pair item in group at index.
	 * @param {Hash} item
	 * @param {Int} index
	 */
	insertItemAt: function(item, index)
	{
		return Kekule.ArrayUtils.insertUniqueEx(this.getItems(), item, index).index;
	},
	/**
	 * Remove an attrib-object pair item at index in group.
	 * @param {Int} index
	 */
	removeItemAt: function(index)
	{
		var item = this.getItems()[index];
		if (item)
		{
			this.getItems().splice(index, 1);
			this.notifyItemsChanged();
		}
	},
	/**
	 * Remove an attrib-object pair item in group.
	 * @param {Kekule.ChemStructureObject} obj
	 */
	removeItem: function(item)
	{
		var index = this.getItems().indexOf(item);
		if (index >= 0)
			this.removeItemAt(index);
	},

	/**
	 * Get object stored in item at index.
	 * @param {Int} index
	 * @return {Object}
	 */
	getObjAt: function(index)
	{
		var item = this.getItemAt(index);
		return item? item.obj: null;
	},
	/**
	 * Insert an attrib-object pair item in group at index.
	 * @param {Hash} item
	 * @param {Int} index
	 * @param {Hash} attributes Additional attributes of this object. Can be null.
	 */
	insertObj: function(obj, index, attributes)
	{
		return this.insertObjAt(obj, index, attributes);
	},
	/**
	 * Insert an attrib-object pair item in group at index.
	 * @param {Hash} item
	 * @param {Int} index
	 * @param {Hash} attributes Additional attributes of this object. Can be null.
	 */
	insertObjAt: function(obj, index, attributes)
	{
		if (!obj)
			return;
		if (this.indexOfObj(obj) >= 0) // already exists
			;// do nothing
		else
		{
			if (this.ensureItemClass(obj))
			{
				var item = {'obj': obj};
				this.beginUpdate();
				try
				{
					var result = this.insertItem(item, index);
					if (attributes)
						this.setAttributesAt(result, attributes);
					this.notifyItemsChanged();
				}
				finally
				{
					this.endUpdate();
				}
				return result;
			}
		}
	},
	/**
	 * Add object to group. If object already in group, nothing will be done.
	 * @param {Kekule.ChemStructureObject} obj
	 * @param {Hash} attributes Additional attributes of this object. Can be null.
	 */
	appendObj: function(obj, attributes)
	{
		/*
		if (!obj)
			return;
		if (this.indexOfObj(obj) >= 0) // already exists
			;// do nothing
		else
		{
			if (this.ensureItemClass(obj))
			{
				var item = {'obj': obj};
				var result = this.getItems().push(item);
				if (attributes)
					this.setAttributesAt(this.getItems().length - 1, attributes);
				this.notifyItemsChanged();
				return result;
			}
		}
		*/
		return this.insertObj(obj, null, attributes);
	},
	/**
	 * Remove object at index in group.
	 * @param {Int} index
	 */
	removeObjAt: function(index)
	{
		var item = this.getItems()[index];
		if (item)
		{
			//this.getItems().removeAt(index);
			this.getItems().splice(index, 1);
			this.notifyItemsChanged();
		}
	},
	/**
	 * Remove an object in group.
	 * @param {Kekule.ChemStructureObject} obj
	 */
	removeObj: function(obj)
	{
		var index = this.indexOfObj(obj);
		if (index >= 0)
			this.removeObjAt(index);
	},

	/**
	 * Returns an array containing the child objects (without attribute).
	 * @returns {Array}
	 */
	getAllObjs: function()
	{
		var result = [];
		for (var i = 0, l = this.getItemCount(); i < l; ++i)
		{
			result.push(this.getObjAt(i));
		}
		return result;
	},

	/** @ignore */
	getChildSubgroupNames: function(/*$super*/)
	{
		return ['item'].concat(this.tryApplySuper('getChildSubgroupNames')  /* $super() */);
	},
	/** @ignore */
	getBelongChildSubGroupName: function(obj)
	{
		return 'item';
	},
	/*
	 * Get count of child objects in root.
	 * @returns {Int}
	 */
	/*
	getChildCount: function()
	{
		return this.getItemCount();
	},
	*/
	/*
	 * Get child object at index.
	 * @param {Int} index
	 * @returns {Variant}
	 */
	/*
	getChildAt: function(index)
	{
		return this.getObjAt(index);
	},
	*/
	/*
	 * Get the index of obj (object or object-attribute item) in children list of root.
	 * @param {Variant} obj
	 * @returns {Int} Index of obj or -1 when not found.
	 */
	/*
	indexOfChild: function(obj)
	{
		var result = this.indexOfObj(obj);
		if (result < 0)
			result = this.indexOfItem(obj);
		return result;
	},
	*/
	/**
	 * Returns next sibling item to child item or object.
	 * @param {Variant} child Item or object.
	 * @returns {Hash}
	 */
	getNextSiblingOfChild: function(child)
	{
		var refIndex = this.indexOfItem(child);
		if (refIndex < 0)
			refIndex = this.indexOfObj(child);
		return (refIndex >= 0)? this.getChildAt(refIndex + 1): null;
	},
	/**
	 * Append obj to children list. If obj already inside, nothing will be done.
	 * @param {Object} obj
	 * @returns {Int} Index of obj after appending.
	 */
	appendChild: function(obj)
	{
		if (obj instanceof Kekule.ChemObject)
			return this.appendObj(obj);
		else  // item
			return this.appendItem(obj);
	},
	/**
	 * Insert obj to index of children list. If obj already inside, its position will be changed.
	 * @param {Object} obj
	 * @param {Object} index
	 * @return {Int} Index of obj after inserting.
	 */
	insertChild: function(obj, index)
	{
		if (obj instanceof Kekule.ChemObject)
			return this.insertObj(obj, index);
		else  // item
			return this.insertItem(obj, index);
	},
	/**
	 * Insert attrib-object pair item before refChild in group.
	 * If refChild is null or does not exists, item will be append to tail of list.
	 * @param {Hash} item
	 * @param {Hash} refItem
	 */
	insertBefore: function(item, refChild)
	{
		var refIndex = this.indexOfItem(refChild);
		if (refIndex < 0)
			refIndex = this.indexOfObj(refChild);
		return this.insertChild(item, refIndex);
	}
	/*
	 * Remove a child at index.
	 * @param {Int} index
	 * @returns {Variant} Child object removed.
	 */
	/*
	removeChildAt: function(index)
	{
		return this.removeItemAt(index);
	},
	*/
	/*
	 * Remove an object or attrib-object pair item from group.
	 * @param {Variant} obj
	 */
	/*
	removeChild: function($super, obj)
	{
		var result;
		var index = this.indexOfItem(obj);
		if (index <= 0)
			index = this.indexOfObj(obj);
		if (index >= 0)
			result = this.removeItemAt(index);
		return result || $super(obj);
	}
	*/
});

/**
 * A group of structure fragment.
 * @class
 * @augments Kekule.ChemStructureObjectGroup
 */
Kekule.StructureFragmentGroup = Class.create(Kekule.ChemStructureObjectGroup,
/** @lends Kekule.ChemStructureObjectGroup# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructureObjectGroup',
	/** @private */
	ITEM_BASE_CLASSES: ['Kekule.StructureFragment', 'Kekule.StructureFragmentGroup'] // the base class of each items in group
});

/**
 * A group of molecule.
 * @class
 * @augments Kekule.ChemStructureObjectGroup
 */
Kekule.MoleculeGroup = Class.create(Kekule.ChemStructureObjectGroup,
/** @lends Kekule.MoleculeGroup# */
{
	/** @private */
	CLASS_NAME: 'Kekule.MoleculeGroup',
	/** @private */
	ITEM_BASE_CLASSES: ['Kekule.Molecule', 'Kekule.MoleculeGroup'] // the base class of each items in group
});

/**
 * A molecule composited by a few sub molecules.
 * @class
 * @augments Kekule.Molecule
 * @param {String} id Id of this molecule.
 * @param {String} name Name of molecule.
 *
 * @property {String} name Name of molecule.
 */
Kekule.CompositeMolecule = Class.create(Kekule.Molecule,
/** @lends Kekule.CompositeMolecule# */
{
	/** @private */
	CLASS_NAME: 'Kekule.CompositeMolecule',
	/**
	 * @constructs
	 */
	initialize: function(/*$super, */id, name)
	{
		this.tryApplySuper('initialize', [id, name])  /* $super(id, name) */;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('subMolecules', {
			'dataType': 'Kekule.MoleculeGroup',
			'getter': function()
				{
					if (!this.getPropStoreFieldValue('subMolecules'))
						this.setPropStoreFieldValue('subMolecules', new Kekule.MoleculeGroup());
					return this.getPropStoreFieldValue('subMolecules');
				}});
	},

	/** @ignore */
	doGetComparisonPropNames: function(/*$super, */options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options])  /* $super(options) */;
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			result.push('subMolecules');
		}
		return result;
	},

	/** @private */
	ownerChanged: function(/*$super, */newOwner, oldOwner)
	{
		var subMols = this.getPropStoreFieldValue('subMolecules');
		if (subMols)
			subMols.setOwner(newOwner);
		this.tryApplySuper('ownerChanged', [newOwner, oldOwner])  /* $super(newOwner, oldOwner) */;
	},

	/**
	 * Returns the number of sub molecules.
	 * @returns {Int}
	 */
	getSubMoleculeCount: function()
	{
		var subs = this.getPropStoreFieldValue('subMolecules');
		return subs? subs.length: 0;
	},

	// as composite molecule are consisted by individual molecules, itself has no ctab nor formula
	/**
	 * Check whether a connection table is used to represent this fragment.
	 * @returns {Bool}
	 */
	hasCtab: function()
	{
		return false;
	},
	/**
	 * Check whether a formula is used to represent this fragment.
	 * @returns {Bool}
	 */
	hasFormula: function()
	{
		return false;
	},
	/**
	 * Return all bonds in structure as well as in sub structure.
	 * @returns {Array} Array of {Kekule.ChemStructureConnector}.
	 */
	getAllContainingConnectors: function()
	{
		var subMols = this.getSubMolecules();
		var result = [];
		for (var i = 0, l = subMols.getItemCount(); i < l; ++i)
		{
			var m = subMols.getObjAt(i);
			if (m.getAllContainingConnectors)
			{
				var a = m.getAllContainingConnectors();
				if (a && a.length)
					result = result.concat(a);
			}
		}
		return result;
	},

	/**
	 * Calculate the box to fit all sub molecule.
	 * @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
	 * @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
	 */
	getContainerBox: function(coordMode)
	{
		var result = null;
		var subMols = this.getPropStoreFieldValue('subMolecules');
		if (subMols)
		{
			for (var i = 0, l = subMols.getItemCount(); i < l; ++i)
			{
				var mol = subMols.getObjAt(i);
				var box = mol.getContainerBox(coordMode);
				if (box)
				{
					if (!result)
						result = Kekule.BoxUtils.clone(box); //Object.extend({}, box);
					else
						result = Kekule.BoxUtils.getContainerBox(result, box);
				}
			}
		}
		return result;
	},

	/** @private */
	doGetCtab: function() { return null; },
	/** @private */
	doSetCtab: function() {},
	/** @private */
	doGetFormula: function() { return null; },
	/** @private */
	doSetFormula: function() {},

	/** @ignore */
	getChildSubgroupNames: function(/*$super*/)
	{
		return ['subMolecule'].concat(this.tryApplySuper('getChildSubgroupNames')  /* $super() */);
	},
	/** @ignore */
	getBelongChildSubGroupName: function(/*$super, */obj)
	{
		if (obj instanceof Kekule.Molecule)
			return 'subMolecule';
		else
			return this.tryApplySuper('getBelongChildSubGroupName', [obj])  /* $super(obj) */;
	},
	/*
	 * Get count of child molecules.
	 * @returns {Int}
	 */
	/*
	getChildCount: function()
	{
		return this.getSubMolecules().getChildCount();
	},
	*/
	/*
	 * Get child object at index.
	 * @param {Int} index
	 * @returns {Variant}
	 */
	/*
	getChildAt: function(index)
	{
		return this.getSubMolecules().getChildAt(index);
	},
	*/
	/*
	 * Get the index of obj in children list of root.
	 * @param {Variant} obj
	 * @returns {Int} Index of obj or -1 when not found.
	 */
	/*
	indexOfChild: function(obj)
	{
		return this.getSubMolecules().indexOfChild(obj);
	},
	*/
	/**
	 * Returns next sibling object to childObj.
	 * @param {Object} childObj
	 * @returns {Object}
	 */
	getNextSiblingOfChild: function(childObj)
	{
		return this.getSubMolecules().getNextSiblingOfChild(childObj);
	},
	/*
	 * Append obj to children list of root. If obj already inside, nothing will be done.
	 * @param {Object} obj
	 * @returns {Int} Index of obj after appending.
	 */
	/*
	appendChild: function($super, obj)
	{
		return $super(obj);
		//return this.getSubMolecules().appendChild(obj);
	},
	*/
	/**
	 * Insert obj to index of children list of root. If obj already inside, its position will be changed.
	 * @param {Object} obj
	 * @param {Object} index
	 * @return {Int} Index of obj after inserting.
	 */
	insertChild: function(obj, index)
	{
		//return this.getSubMolecules().insertChild(obj, index);
		return this.insertChildAt(obj, index);
	},
	/*
	 * Insert obj before refChild in list of root. If refChild is null or does not exists, obj will be append to tail of list.
	 * @param {Object} obj
	 * @param {Object} refChild
	 * @return {Int} Index of obj after inserting.
	 */
	/*
	insertBefore: function($super, obj, refChild)
	{
		if (obj instanceof Kekule.StructureFragment)
			return this.getSubMolecules().insertBefore(obj, refChild);
		else
			return $super(obj, refChild);
	},
	*/
	/*
	 * Remove a child at index.
	 * @param {Int} index
	 * @returns {Variant} Child object removed.
	 */
	/*
	removeChildAt: function(index)
	{
		return this.getSubMolecules().removeChildAt(index);
	},
	*/
	/*
	 * Remove obj from children list of root.
	 * @param {Variant} obj
	 * @returns {Variant} Child object removed.
	 */
	/*
	removeChild: function($super, obj)
	{
		return this.getSubMolecules().removeChild(obj) || $super(obj);
	}
	*/
});

/**
 * A list of molecule.
 * @class
 * @augments Kekule.ChemObjList
 * @param {String} id Id of list.
 */
Kekule.MoleculeList = Class.create(Kekule.ChemObjList,
/** @lends Kekule.MoleculeList# */
{
	/** @private */
	CLASS_NAME: 'Kekule.MoleculeList',
	/** @private */
	/** @constructs */
	initialize: function(/*$super, */id)
	{
		this.tryApplySuper('initialize', [id, Kekule.Molecule])  /* $super(id, Kekule.Molecule) */;
	}
});

/**
 * Class to store label strings of different structure node (e.g., atom list, RGroup...).
 * @class
 */
Kekule.ChemStructureNodeLabels = {
	/** Whether display isotope alias (e.g., D instead of 2H). */
	ENABLE_ISOTOPE_ALIAS: true,

	/** Label for unset element. */
	UNSET_ELEMENT: '?',

	/* Label for deuterium */
	//DEUTERIUM: 'D',

	// for Pseudoatom
	/** Label for dummy atom. */
	DUMMY_ATOM: 'Du',
	/** Label for Non C/H atom. */
	HETERO_ATOM: 'Q',
	/** Label for Unspecific atom */
	ANY_ATOM: 'A',
	/** Label for Custom atom */
	CUSTOM_ATOM: '@',

	// for VariableAtom
	/** Label for Unspecific atom */
	VARIABLE_ATOM: 'L',

	/** Default label for sub group */
	SUBGROUP: 'R',

	// for VariableAtom
	/** Display isotope list in bracket, such as [H, 13C, O, P]. */
	ISO_LIST_LEADING_BRACKET: '[',
	/** Display isotope list in bracket, such as [H, 13C, O, P]. */
	ISO_LIST_TAILING_BRACKET: ']',
	/** Default delimiter for each isotope in list */
	ISO_LIST_DELIMITER: ',',
	/** Default prefix to indicate it is a disallow list. */
	ISO_LIST_DISALLOW_PREFIX: 'NOT'
}

/**
 * A factory class to create suitable structure node by node symbol (atomic symbol, R, L and so on).
 * @class
 */
Kekule.ChemStructureNodeFactory = {
	/** @private */
	CANDIDATE_CLASSES: [Kekule.SubGroup, Kekule.VariableAtom, Kekule.Pseudoatom /*, Kekule.Atom*/],

	getClassByLabel: function(label, defaultClass)
	{
		if (defaultClass === undefined)
		{
			defaultClass = Kekule.Pseudoatom;
		}
		var NL = Kekule.ChemStructureNodeLabels;
		var candidateLabels = [
			[NL.SUBGROUP],
			[NL.VARIABLE_ATOM],
			[NL.DUMMY_ATOM, NL.HETERO_ATOM, NL.ANY_ATOM, NL.CUSTOM_ATOM]
		];
		var classes = Kekule.ChemStructureNodeFactory.CANDIDATE_CLASSES;
		var cclass;
		for (var i = 0, l = classes.length; i < l; ++i)
		{
			var c = classes[i];
			var suitLabels = candidateLabels[i];
			if (suitLabels.indexOf(label) >= 0)  // class found
			{
				cclass = c;
				break;
			}
		}
		if (!cclass)  // class not found, check if it is atom or use default one
		{
			/*
			if (label === Kekule.ChemStructureNodeLabels.DEUTERIUM)
				cclass = Kekule.Atom;
			else
			*/
			if (Kekule.IsotopesDataUtil.isIsotopeIdAvailable(label))
				cclass = Kekule.Atom;
			else
				cclass = defaultClass;
		}
		return cclass;
	},

	createByLabel: function(label)
	{
		var cclass = Kekule.ChemStructureNodeFactory.getClassByLabel(label);
		var result = new cclass();
		if (result instanceof Kekule.Pseudoatom)
			result.setSymbol(label);
		else if (result instanceof Kekule.Atom)
		{
			result.setIsotopeId(label);
		}
		return result;
	}
}

})();
