Source: algorithm/kekule.structures.stereos.js

/**
 * @fileoverview
 * Basic classes and methods related with stereo chemistry.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /core/kekule.structures.js
 * requires /utils/kekule.utils.js
 * requires /algorithm/kekule.structures.comparers.js
 * requires /algorithm/kekule.structure.canonicalizers.js
 * requires /algorithm/kekule.structure.ringSearches.js
 */

(function(){
"use strict";

var AU = Kekule.ArrayUtils;
var CU = Kekule.CoordUtils;

	/**
	 * Enumeration of rotation directions.
	 * @enum
	 */
Kekule.RotationDir = {
	CLOCKWISE: 1,
	ANTICLOCKWISE: -1,
	UNKNOWN: 0
};
var RD = Kekule.RotationDir;

/**
 * Default options to do stereo identification.
 * @object
 */
Kekule.globalOptions.add('algorithm.stereoPerception', {
	useFlatternedShadow: true,
	perceiveStereoConnectors: true,
	perceiveChiralNodes: true,
	calcParity: true
});

/**
 * Util class about stereo chemistry.
 * @class
 */
Kekule.MolStereoUtils = {
	/** @private */
	FISCHER_PROJECTION_BOND_ALLOWED_ERROR: 0.08,
	/**
	 * Returns dihedral angle of plane (n1, n2, n3) and (n2, n3, n4).
	 * @param {Kekule.ChemStructureNode} n1
	 * @param {Kekule.ChemStructureNode} n2
	 * @param {Kekule.ChemStructureNode} n3
	 * @param {Kekule.ChemStructureNode} n4
	 * @param {Int} coordMode
	 * @param {Bool} allowCoordBorrow Default is true.
	 * @returns {Float}
	 */
	getDihedralAngleOfNodes: function(n1, n2, n3, n4, coordMode, allowCoordBorrow)
	{
		if (Kekule.ObjUtils.isUnset(allowCoordBorrow))
			allowCoordBorrow = true;

		if (Kekule.ObjUtils.isUnset(coordMode))
		{
			if (n2.hasCoord3D())
				coordMode = Kekule.CoordMode.COORD3D;  // default use 3D coord to calculate
			else
				coordMode = Kekule.CoordMode.COORD2D;
		}

		var getNodeCoord = Kekule.MolStereoUtils._getNodeCoordForBondParity;

		var axisBond = n2.getConnectorTo(n3);
		var axisIsDoubleBond = axisBond && axisBond.isDoubleBond && axisBond.isDoubleBond();

		var c2 = getNodeCoord(n2, null, null, coordMode, allowCoordBorrow);
		var c3 = getNodeCoord(n3, null, null, coordMode, allowCoordBorrow);
		var c1 = getNodeCoord(n1, n2, c2, coordMode, allowCoordBorrow, axisIsDoubleBond);
		var c4 = getNodeCoord(n4, n3, c3, coordMode, allowCoordBorrow, axisIsDoubleBond);
		if (c1 && c2 && c3 && c4)
		{
			var result = Kekule.GeometryUtils.getDihedralAngleOfPoints(c1, c2, c3, c4);
			//console.log('dihedral', result * 180 / Math.PI, c1, c2, c3, c4);
			return result;
		}
		else  // null in coord, c1-4, can not calculate angle
			return -1;
	},

	/**
	 * Returns the stereo parity of node sequence n1 to n4.
	 * @param {Kekule.ChemStructureNode} n1
	 * @param {Kekule.ChemStructureNode} n2
	 * @param {Kekule.ChemStructureNode} n3
	 * @param {Kekule.ChemStructureNode} n4
	 * @param {Int} coordMode
	 * @param {Bool} allowCoordBorrow
	 * @returns {Int} If n1 and n4 at the same side of line n2-n3, parity is odd(1), otherwise even(2) will be returned.
	 */
	getParityOfNodeSeq: function(n1, n2, n3, n4, coordMode, allowCoordBorrow)
	{
		var SP = Kekule.StereoParity;
		var angle = Kekule.MolStereoUtils.getDihedralAngleOfNodes(n1, n2, n3, n4, coordMode, allowCoordBorrow);
		var result = (angle < 0)? SP.UNKNOWN:
			(angle < Math.PI * 2 / 5 || angle > Math.PI * 8 / 5)? SP.ODD:
				(angle > Math.PI * 3 / 5 && angle < Math.PI * 7 / 5)? SP.EVEN:
					SP.UNKNOWN;
		return result;
	},

	/** @private */
	_getNodeCoordForBondParity: function(node, centerNode, centerCoord, coordMode, allowCoordBorrow, axisIsDoubleBond)
	{
		if (coordMode !== Kekule.CoordMode.COORD2D)  // 3D, get 3D absolute coord directly
			return node.getAbsCoordOfMode(coordMode, true);  // allow borrow
		else  // coord 2D, add z value, consider wedge bonds
		{
			var result = node.getAbsCoordOfMode(coordMode, true);  // allow borrow
			if (centerNode && centerCoord)
			{
				var connector = node.getConnectorTo(centerNode);
				if (connector.getStereo)
				{
					var bondStereo = connector.getStereo();
					var BS = Kekule.BondStereo;
					var wedgeDirs = [BS.UP, BS.UP_INVERTED, BS.DOWN, BS.DOWN_INVERTED];
					if (wedgeDirs.indexOf(bondStereo) >= 0)
					{
						var index = connector.indexOfConnectedObj(node) - connector.indexOfConnectedObj(centerNode);
						if (index < 0)
							bondStereo = BS.getInvertedDirection(bondStereo);
						var zFactors = [1, -1, -1, 1];
						var distance = CU.getDistance(result, centerCoord);
						result.z = distance * zFactors[wedgeDirs.indexOf(bondStereo)];
						//console.log(node.getId(), result);
						/*
						 if (axisIsDoubleBond)
						 result.y = 0;
						 */
					}
					else if ([BS.UP_OR_DOWN, BS.UP_OR_DOWN_INVERTED].indexOf(bondStereo) >= 0)  // direction not certain
						return null;  // return a special mark, can determinate angle calculation
				}
			}
			result.z = result.z || 0;
			return result;
		}
	},

	/** @private */
	_calcParityOfCoordPairs: function(c1, c2, a1, a2, b1, b2, strictStereoBondGeometry)
	{
		var SP = Kekule.StereoParity;
		var axisVector = CU.substract(c2, c1);  // vector of bond
		//console.log('input coords', c1, c2, a1, a2, b1, b2, axisVector);
		if (!axisVector.x && !axisVector.y)  // c1, c2 are same, can not calc parity
			return SP.UNKNOWN;
		var axisAngle = Math.atan2(axisVector.y, axisVector.x);
		// rotate bond to X axis and align to zero
		var matrix = CU.calcTransform2DMatrix({
			'rotateAngle': -axisAngle, 'center': c1,
			'translateX': -c1.x, 'translateY': -c1.y
		});
		var originCoords = [c1, c2, a1, a2, b1, b2];
		var transformedCoords = [];
		for (var i = 0, l = originCoords.length; i < l; ++i)
		{
			var coord = originCoords[i];
			if (coord)
				transformedCoords[i] = CU.transform2DByMatrix(coord, matrix);
			else
				transformedCoords[i] = null;
		}
		var tc1 = transformedCoords[0], tc2 = transformedCoords[1];
		//var bondDirection = Math.sign(tc2.x);
		var ta1 = transformedCoords[2], ta2 = transformedCoords[3];  // tc1 is [0, 0] after transform
		var tb1 = CU.substract(transformedCoords[4], tc2), tb2 = transformedCoords[5]? CU.substract(transformedCoords[5], tc2): null;

		// compare direction of ta1/ta2, tb1/tb2
		var getDirectionSign = function(v1, v2, threshold, refDir)
		{
			var result = 0;
			var lenV1 = Math.sqrt(Math.sqr(v1.x) + Math.sqr(v1.y));
			if (!v2)
			{
				var d = v1.y / lenV1;
				if (Math.abs(d) < threshold)
					result = 0;
				else
					result = Math.sign(v1.y);
			}
			else
			{
				var lenV2 = Math.sqrt(Math.sqr(v2.x) + Math.sqr(v2.y));
				var d = v1.y / lenV1 - v2.y / lenV2;   // standard y coord
				if (Math.abs(d) < threshold)
					result = 0;
				else
				{
					if (Math.sign(v1.y) !== Math.sign(v2.y))  // v1, v2 or different side of bond (x-axis)
					{
						result = Math.sign(d) * refDir;
					}
					else  // v1, v2 on same side
					{
						if (strictStereoBondGeometry)
							return 0;
						else
						{
							var ctg1 = v1.x / v1.y;
							var ctg2 = v2.x / v2.y;
							d = -(ctg1 - ctg2);
							result = Math.sign(d);
						}
					}
				}
			}
			return result;
		};
		var threshold = 5e-2; // CU.getDistance(c2, c1) / 1e3;
		var sa = getDirectionSign(ta1, ta2, threshold, -1);
		var sb = getDirectionSign(tb1, tb2, threshold, 1);

		var result;
		if (!sa || !sb)
			result = SP.UNKNOWN;
		else if (sa === sb)  // ta1/tb1 on same side of bond
			result = SP.ODD;
		else
			result = SP.EVEN;

		//console.log('compare', tc1, tc2, ta1, ta2, tb1, tb2, sa, sb, result);
		//console.log('compare', sa, sb, result);
		return result;
	},

	/**
	 * Check if a connector is a double bond that connects with two different groups on each end (may has trans or cis configurature).
	 * Note: the parent structure should be canonicalized before calling this method.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Bool}
	 */
	isStereoBond: function(connector)
	{
		var isUnset = Kekule.ObjUtils.isUnset;
		if (connector.getBelongedRingMinSize)
		{
			var ringSize = connector.getBelongedRingMinSize() || 0;
			if (ringSize > 0 && ringSize <= 10)  // connector in small ring, never has configurature problem
				return false;
		}
		var result = false;
		var isDouble = connector.isDoubleBond && connector.isDoubleBond();
		if (isDouble && (connector.getConnectedObjCount() === 2))
		{
			var endNodes = connector.getConnectedChemNodes(); // [connector.getConnectedObjAt(0), connector.getConnectedObjAt(1)];
			if (endNodes.length === 2)
			{
				var result = true;
				for (var i = 0; i < 2; ++i)
				{
					var node = endNodes[i];
					var hydroCount = node.getHydrogenCount(true);  // include explicit bonded H atoms
					var sideObjs = AU.exclude(node.getLinkedChemNodes(), endNodes);
					if ((hydroCount >= 2) || (!sideObjs.length))
					{
						result = false;
						break;
					}
					if (/*hydroCount === 1 && */ sideObjs.length === 1)  // N=N double bond may has no hydro count
					{
						// result = true;
					}
					else if (sideObjs.length === 2)
					{
						if (isUnset(sideObjs[0].getCanonicalizationIndex()) && isUnset(sideObjs[1].getCanonicalizationIndex())
							|| (sideObjs[0].getCanonicalizationIndex() === sideObjs[1].getCanonicalizationIndex()))
						{
							result = false;
							break;
						}
					}

					// check side bond, if not single, no stereo
					for (var j = 0, k = sideObjs.length; j < k; ++j)
					{
						var bond = node.getConnectorTo(sideObjs[j]);
						if (!bond.isSingleBond || !bond.isSingleBond())
						{
							result = false;
							break;
						}
					}
					if (!result)
						break;
				}
			}
		}
		else
			result = false;
		return result;
	},
	/**
	 * Find stereo double bond in struct fragment or ctab.
	 * Note, before finding, if param ignoreCanonicalization is false,
	 * the struct fragment will be canonicalized by morgan algorithm to set node cano index.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Bool} ignoreCanonicalization
	 * @returns {Array}
	 * @private
	 */
	doFindStereoBonds: function(structFragmentOrCtab, ignoreCanonicalization)
	{
		var result = [];
		if (!ignoreCanonicalization)
			Kekule.canonicalizer.canonicalize(structFragmentOrCtab, 'morganEx');
		for (var i = 0, l = structFragmentOrCtab.getConnectorCount(); i < l; ++i)
		{
			var conn = structFragmentOrCtab.getConnectorAt(i);
			if (Kekule.MolStereoUtils.isStereoBond(conn))
				result.push(conn);
		}
		return result;
	},

	/**
	 * Returns essential nodes to determine the stereo of a double bond.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Array} Array of nodes.
	 */
	getStereoBondKeyNodes: function(connector)
	{
		var result = null;
		if (connector.getConnectedObjCount() === 2)
		{
			var endNodes = connector.getConnectedChemNodes();
			if (endNodes.length === 2)
			{
				var refNodes = [];
				for (var i = 0; i < 2; ++i)
				{
					var node = endNodes[i];
					//var hydroCount = node.getHydrogenCount();
					var sideObjs = AU.exclude(node.getLinkedChemNodes(), endNodes);
					if (/*hydroCount === 1 && */ sideObjs.length === 1)  // N=N double bond may has no hydro count
					{
						refNodes.push(sideObjs[0]);
					}
					else if (sideObjs.length === 2)
					{
						var index1 = sideObjs[0].getCanonicalizationIndex() || -1;  // cano index maybe undefined to explicit H atom
						var index2 = sideObjs[1].getCanonicalizationIndex() || -1;
						refNodes.push((index2 > index1)? sideObjs[1]: sideObjs[0]);
					}
					else // too many sideObjs number, maybe a incorrect structure?
					{
						return null;
					}
				}
				result = [refNodes[0], endNodes[0], endNodes[1], refNodes[1]];
			}
		}
		return result;
	},

	/**
	 * Returns all related nodes to determine the stereo of a double bond
	 * (including two bond atoms and four possible connected atoms).
	 * @param {Kekule.ChemStructureConnector} connector
	 * @returns {Array} Array of nodes.
	 *   For structure A1/A2 >C1=C2< B1/B2, the return sequence should be [C1, C2, A1, A2, B1, B2]
	 *   where A1/B1 has superior canonicalization index.
	 */
	getStereoBondRelNodes: function(connector)
	{
		var result = null;
		if (connector.getConnectedObjCount() === 2)
		{
			var endNodes = connector.getConnectedChemNodes();
			if (endNodes.length === 2)
			{
				var relNodes = [];
				for (var i = 0; i < 2; ++i)
				{
					var node = endNodes[i];
					//var hydroCount = node.getHydrogenCount();
					var sideObjs = AU.exclude(node.getLinkedChemNodes(), endNodes);
					if (/*hydroCount === 1 && */ sideObjs.length === 1)  // N=N double bond may has no hydro count
					{
						relNodes.push(sideObjs[0]);
						relNodes.push(null); // an empty node
					}
					else if (sideObjs.length === 2)
					{
						var index1 = sideObjs[0].getCanonicalizationIndex() || -1;  // cano index maybe undefined to explicit H atom
						var index2 = sideObjs[1].getCanonicalizationIndex() || -1;
						relNodes.push((index2 > index1)? sideObjs[1]: sideObjs[0]);
						relNodes.push((index2 > index1)? sideObjs[0]: sideObjs[1]);
					}
					else // too many sideObjs number, maybe a incorrect structure?
					{
						return null;
					}
				}
				result = [endNodes[0], endNodes[1], relNodes[0], relNodes[1], relNodes[2], relNodes[3]];
			}
		}
		return result;
	},

	/**
	 * Calc and returns MDL stereo parity of a stereo double bond.
	 * If two superior group (with larger canonicalization index) at the same side of bond, parity = 1, otherwise parity = 2.
	 * Note: the parent structure fragment should be canonicalized before calculation.
	 * @param {Kekule.ChemStructureConnector} connector
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * //@param {Kekule.StructureFragment} parentMol
	 * @returns {Int} Value from {@link Kekule.StereoParity}.
	 */
	calcStereoBondParity: function(connector, coordMode, ignoreStereoCheck, strictStereoBondGeometry)
	{
		var SP = Kekule.StereoParity;
		if (!ignoreStereoCheck && !Kekule.MolStereoUtils.isStereoBond(connector))
			return SP.NONE;
		if (connector.getStereo && [Kekule.BondStereo.E_OR_Z, Kekule.BondStereo.CIS_OR_TRANS].indexOf(connector.getStereo()) >= 0)
			return SP.NONE;

		var result = SP.UNKNOWN;
		/*
		if (connector.getConnectedObjCount() === 2)
		{
			var endNodes = connector.getConnectedChemNodes(); // [connector.getConnectedObjAt(0), connector.getConnectedObjAt(1)];
			if (endNodes.length === 2)
			{
				var refNodes = [];
				for (var i = 0; i < 2; ++i)
				{
					var node = endNodes[i];
					var hydroCount = node.getHydrogenCount();
					var sideObjs = AU.exclude(node.getLinkedChemNodes(), endNodes);
					if (sideObjs.length === 1)  // N=N double bond may has no hydro count
					{
						refNodes.push(sideObjs[0]);
					}
					else if (sideObjs.length === 2)
					{
						var index1 = sideObjs[0].getCanonicalizationIndex();
						var index2 = sideObjs[1].getCanonicalizationIndex();
						refNodes.push((index2 > index1)? sideObjs[1]: sideObjs[0]);
					}
				}
				result = Kekule.MolStereoUtils.getParityOfNodeSeq(refNodes[0], endNodes[0], endNodes[1], refNodes[1], coordMode);
			}
		}
		*/
		var keyNodes = Kekule.MolStereoUtils.getStereoBondKeyNodes(connector);
		if (keyNodes && keyNodes.length)
		{
			if (Kekule.ObjUtils.isUnset(coordMode))
			{
				if (keyNodes[1].hasCoord3D())
					coordMode = Kekule.CoordMode.COORD3D;  // default use 3D coord to calculate
				else
					coordMode = Kekule.CoordMode.COORD2D;
			}

			/*
			result = Kekule.MolStereoUtils.getParityOfNodeSeq(keyNodes[0], keyNodes[1], keyNodes[2], keyNodes[3], coordMode);
			return result;
			*/
			if (coordMode === Kekule.CoordMode.COORD3D)
			{
				var angle = Kekule.MolStereoUtils.getDihedralAngleOfNodes(keyNodes[0], keyNodes[1], keyNodes[2], keyNodes[3], coordMode);
				if (angle < 0 || (angle > Math.PI * 2 / 5 && angle < Math.PI * 3 / 5) || (angle > Math.PI * 7 / 5 && angle < Math.PI * 8 / 5))
					return SP.UNKNOWN;  // dihedral angle identity not clear enough
			}

			var allowCoordBorrow = true;  // allow coord borrow
			var getNodeCoord = Kekule.MolStereoUtils._getNodeCoordForBondParity;
			var extractCoord = function(coord, coordAxises)
			{
				if (!coord)
					return null;
				else
					return {
						x: coord[coordAxises[0]],
						y: coord[coordAxises[1]]
					}
			};

			// if pass dihedral test, check parity on X/Y, X/Z, X/Z planet projection
			var relNodes = Kekule.MolStereoUtils.getStereoBondRelNodes(connector);
			// For structure A1/A2 >C1=C2< B1/B2
			var relCoords = [
				getNodeCoord(relNodes[0], null, null, coordMode, allowCoordBorrow),  // C1
				getNodeCoord(relNodes[1], null, null, coordMode, allowCoordBorrow)   // C2
			];
			relCoords = relCoords.concat([
				getNodeCoord(relNodes[2], relNodes[0], relCoords[0], coordMode, allowCoordBorrow, true),  // A1
				relNodes[3]? getNodeCoord(relNodes[3], relNodes[0], relCoords[0], coordMode, allowCoordBorrow, true): null, // A2
				getNodeCoord(relNodes[4], relNodes[1], relCoords[1], coordMode, allowCoordBorrow, true),  // B1
				relNodes[5]? getNodeCoord(relNodes[5], relNodes[1], relCoords[1], coordMode, allowCoordBorrow, true): null  // B2
			]);

			//console.log('---------- calc parity --------------');

			var prjPlanets = (coordMode === Kekule.CoordMode.COORD2D)? [['x', 'y']]:  [['x', 'y'], ['x', 'z'], ['y', 'z']];
			var prjResult = null;
			for (var i = 0, l = prjPlanets.length; i < l; ++i)
			{
				var currPrjCoords = [];
				for (var j = 0, k = relCoords.length; j < k; ++j)
				{
					if (relCoords[j])
						currPrjCoords.push(extractCoord(relCoords[j], prjPlanets[i]));
					else
						currPrjCoords.push(null);
				}
				var prjParity = Kekule.MolStereoUtils._calcParityOfCoordPairs(
						currPrjCoords[0], currPrjCoords[1], currPrjCoords[2], currPrjCoords[3], currPrjCoords[4], currPrjCoords[5],
						strictStereoBondGeometry
				);
				if (prjParity !== SP.UNKNOWN)
				{
					if (prjResult === null)
						prjResult = prjParity;
					else if (prjResult !== prjParity)  // parity not match on different planet
						return SP.UNKNOWN;
				}
			}
			// pass planet projection check
			result = prjResult;
		}
		return result;
	},

	/**
	 * Detect and mark parity of all stereo bonds in structure fragment.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization If false, ctab will be canonicalized before perception.
	 * @returns {Array} Array of all chiral nodes.
	 * @private
	 */
	doPerceiveStereoConnectors: function(structFragmentOrCtab, coordMode, ignoreCanonicalization, strictStereoBondGeometry)
	{
		var result = Kekule.MolStereoUtils.doFindStereoBonds(structFragmentOrCtab, ignoreCanonicalization);
		structFragmentOrCtab.beginUpdate();
		try
		{
			if (result && result.length)
			{
				for (var i = 0, l = result.length; i < l; ++i)
				{
					var c = result[i];
					var parity = Kekule.MolStereoUtils.calcStereoBondParity(c, coordMode, true, strictStereoBondGeometry);
					if (c.setParity)
						c.setParity(parity);
				}
			}
		}
		finally
		{
			structFragmentOrCtab.endUpdate();
		}
		return result;
	},

	/**
	 * Check if a chem structure node is a chiral one.
	 * Note: now only C/Si/S/P/N/B atoms are considered, and the parent structure fragment may be standardized.
	 * @param {Kekule.ChemStructureNode} node
	 */
	isChiralNode: function(node)
	{
		var result = false;
		if (node.mayContainElement)  // is abstract atom
		{
			var diffNeighborCount;
			var maxMultiBondCount;
			var possibleCharges = null;
			var mayBeChiral = false;
			// check C first
			if (node.mayContainElement('C') || node.mayContainElement('Si'))  // must has four different attached groups
			{
				diffNeighborCount = 4;
				maxMultiBondCount = 0;
				possibleCharges = [0];
				mayBeChiral = true;
			}
			else if (node.mayContainElement('N'))  // must has four different attached groups
			{
				diffNeighborCount = 4;
				maxMultiBondCount = 0;
				mayBeChiral = true;
			}
			else if (node.mayContainElement('S') || node.mayContainElement('P'))  // must has three different attached groups
			{
				diffNeighborCount = 3;
				maxMultiBondCount = 0;
				mayBeChiral = true;
			}
			else if (node.mayContainElement('B'))
			{
				diffNeighborCount = 3;
				maxMultiBondCount = 0;
				possibleCharges = [-1];
				mayBeChiral = true;
			}

			if (mayBeChiral)
			{
				var neighbors = node.getLinkedChemNodes();
				var multibonds = node.getLinkedMultipleBonds();
				var hydroCount = node.getHydrogenCount ? node.getHydrogenCount() : 0;
				var allHydroCount = node.getHydrogenCount ? node.getHydrogenCount(true) : 0;
				var charge = node.getCharge() || 0;
				//console.log(charge);
				if (multibonds.length > maxMultiBondCount || allHydroCount >= 2 || neighbors.length + hydroCount < diffNeighborCount
					|| (possibleCharges && possibleCharges.indexOf(charge) < 0))
					result = false;
				else  // check cano index of neighbors, see whether they are different
				{
					result = true;
					var canoIndexCounts = [];
					for (var i = 0, l = neighbors.length; i < l; ++i)
					{
						var n = neighbors[i];
						var canoIndex = n.getCanonicalizationIndex() || 0;
						if (!canoIndexCounts[canoIndex])
							canoIndexCounts[canoIndex] = 1;
						else  // duplicate canoIndex
						{
							result = false;
							break;
						}
					}
				}
			}
		}

		return result;
	},

	/**
	 * Find chiral nodes in struct fragment or ctab.
	 * Note, before finding, if param ignoreCanonicalization is false,
	 * the struct fragment will be canonicalized by morgan algorithm to set node cano index.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Bool} ignoreCanonicalization
	 * @returns {Array}
	 * @private
	 */
	doFindChiralNodes: function(structFragmentOrCtab, ignoreCanonicalization)
	{
		var result = [];
		//structFragment.canonicalize('morgan');
		if (!ignoreCanonicalization)
			Kekule.canonicalizer.canonicalize(structFragmentOrCtab, 'morganEx');
		for (var i = 0, l = structFragmentOrCtab.getNodeCount(); i < l; ++i)
		{
			var node = structFragmentOrCtab.getNodeAt(i);
			if (Kekule.MolStereoUtils.isChiralNode(node))
				result.push(node);
		}
		return result;
	},

	/**
	 * Judge the direction of coord1 to coord3 when looking from refCoord to centerCoord
	 * (or put refCoord behide center coord if refCoordBehind param is true).
	 * All coords should have x/y/z values.
	 * @param {Hash} centerCoord
	 * @param {Hash} refCoord
	 * @param {Hash} coord1
	 * @param {Hash} coord2
	 * @param {Hash} coord3
	 * @param {Bool} refCoordBehind
	 * @returns {Int} Value from {@link Kekule.RotationDir}, clockwise, anti-clockwise or unknown.
	 */
	calcRotationDirection: function(centerCoord, refCoord, coord1, coord2, coord3, refCoordBehind)
	{
		// calc the looking direction vector
		var lookingVector = refCoordBehind? CU.substract(centerCoord, refCoord):
			CU.substract(refCoord, centerCoord);
		// now rotate the lookingVector to z-axis
		var rotateAxisVector, rotateAngle;
		var rotateMatrix = null;
		if (!(lookingVector.x === 0 && lookingVector.y === 0))
		{
			rotateAxisVector = Kekule.GeometryUtils.getVectorCrossProduct(lookingVector, {'x': 0, 'y': 0, 'z': 1});
			rotateAngle = Kekule.GeometryUtils.getVectorIncludedAngle(lookingVector, {'x': 0, 'y': 0, 'z': 1});  // get from asin, so always less than 90 degree
			if (lookingVector.z < 0)  // if lookingVector on negative axis of z, the lookingVector should rotate more than 90 degree
				rotateAngle = Math.PI - rotateAngle;
		}
		else if (lookingVector.z < 0)  // lookingVector.x === 0 && lookingVector.y === 0 but z < 0, need to rotate 180 degree
		{
			rotateAxisVector = {'x': 1, 'y': 0, 'z': 0};
			rotateAngle = Math.PI;
		}
		if (rotateAxisVector && rotateAngle)
		{
			rotateMatrix = CU.calcRotate3DMatrix({
				'rotateAngle': rotateAngle,
				'rotateAxisVector': rotateAxisVector,
				'center': {'x': 0, 'y': 0, 'z': 0}
			});
		}
		var v1 = CU.substract(coord1, centerCoord);
		var v2 = CU.substract(coord2, centerCoord);
		var v3 = CU.substract(coord3, centerCoord);
		if (rotateMatrix)
		{
			v1 = CU.transform3DByMatrix(v1, rotateMatrix);
			v2 = CU.transform3DByMatrix(v2, rotateMatrix);
			v3 = CU.transform3DByMatrix(v3, rotateMatrix);
		}
		// now map three rotated vectors to simple X-Y 2D plane, rotate v1 to x axis
		var rotateAngle = -Math.atan2(v1.y, v1.x);
		var rotateMatrix = CU.calcTransform2DMatrix({
			'rotateAngle': rotateAngle,
			'center': {'x': 0, 'y': 0, 'z': 0}
		});
		var c2 = CU.transform2DByMatrix(v2, rotateMatrix);
		var c3 = CU.transform2DByMatrix(v3, rotateMatrix);
		// at last, calc angle of c2 and c3, determinate clockwise or anti
		if (c2.x === 0 || c3.x === 0)  // c2 or c3 also lap with x axis, stereo is unknown
			return RD.UNKNOWN;

		var angle2 = Math.atan2(c2.y, c2.x);
		var angle3 = Math.atan2(c3.y, c3.x);
		if (angle3 < 0)
			angle3 = Math.PI * 2 + angle3;
		if (angle2 < 0)
			angle2 = Math.PI * 2 + angle2;
		return (angle2 < angle3)? RD.ANTICLOCKWISE:
			(angle2 > angle3)? RD.CLOCKWISE:
				RD.UNKNOWN;
	},

	/**
	 * Check if a node is carbon atom in Fischer projection center and returns related information.
	 * Note: only 2D coord will be checked in this function.
	 * @param {Kekule.ChemStructureNode} node
	 * @param {Array} siblings
	 * @param {Hash} options, including:
	 *   {
	 *     allowedError: the allowed error when checking vertical and horizontal line, default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree).
	 *     reversedDirection: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogen: Whether the simplification in saccharide chain form is allowed (H is omitted from structure).
	 *     allowExplicitVerticalHydrogen: Whether the implicit hydrogen on vertical direction is allowed (used in SMILES generation).
	 *     ignoreStructure: Whether structure information should not be checked.
	 *   }
	 * @returns {Hash} The information about this Fischer projection, including:
	 *   {
	 *     horizontalSiblings: array,
	 *     verticalSiblings: array,
	 *     towardSiblings: array, usually siblings on horizontal line,
	 *     awaySiblings: array, usually siblings on vertical line
	 *   }.
	 *   If the node is not a Fischer projection center, null will be returned.
	 * @private
	 */
	_getFischerProjectionInfo: function(node, siblings, options)
	{
		var ops = Object.create(options || null);
		if (siblings.length < 3 || siblings.length > 4)
			return null;
		if (siblings.length === 3 && !ops.allowExplicitHydrogen)
			return null;

		var coordMode = Kekule.CoordMode.COORD2D;
		var allowedError = ops.allowedError || Kekule.MolStereoUtils.FISCHER_PROJECTION_BOND_ALLOWED_ERROR;

		if (!ops.ignoreStructure)  // ensure center node is a C atom with atom symbol hidden
		{
			var isCenterLegal = (node instanceof Kekule.Atom) && (node.getAtomicNumber() === 6);
			if (isCenterLegal && node.getRenderOption)
				isCenterLegal = node.getRenderOption('nodeDisplayMode') !== Kekule.Render.NodeLabelDisplayMode.SHOWN;
			if (!isCenterLegal)
				return null;
		}

		var nodeCoord = node.getAbsCoordOfMode(coordMode, true); // allow borrow

		var nodeSeq = [];  // 0: Top, 1: Right, 2: Bottom: 3: Left
		var verticalNodeCount = 0;

		var _setNodeSeqItem = function(seq, index, value)
		{
			if (seq[index])  // already has item
				return false;
			else
			{
				seq[index] = value;
				return true;
			}
		};

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

			if (!ops.ignoreStructure)  // check bond, must be unstereo one (simple single covalence bond)
			{
				var bond = sibling.getConnectorTo(node);
				var bondStereo = bond.getStereo() || Kekule.BondStereo.NONE;
				var isLegalBond = (bond instanceof Kekule.Bond) && (bondStereo === Kekule.BondStereo.NONE);
				if (!isLegalBond)
					return null;
			}

			var siblingCoord = sibling.getAbsCoordOfMode(coordMode, true);  // allow borrow
			var delta = CU.substract(siblingCoord, nodeCoord);
			var absDeltaX = Math.abs(delta.x);
			var absDeltaY = Math.abs(delta.y);

			if (Kekule.NumUtils.isFloatEqual(absDeltaX, 0) && Kekule.NumUtils.isFloatEqual(absDeltaY, 0))  // x/y too small
				return null;

			var seqIndex;
			var bondLengthRatio;
			if (absDeltaX > absDeltaY)  // on horizontal line
			{
				bondLengthRatio = absDeltaY / absDeltaX;
				seqIndex = (delta.x > 0)? 1: 3;
			}
			else  // on vertical line
			{
				bondLengthRatio = absDeltaX / absDeltaY;
				seqIndex = (delta.y > 0)? 0: 2;
				++verticalNodeCount;
			}
			if (bondLengthRatio > allowedError)  // not vertical
				return null;
			else
			{
				if (!_setNodeSeqItem(nodeSeq, seqIndex, sibling))
					return null;
			}
		}

		// check that must be two vertical nodes (even when H is implicited in saccharide)
		if (verticalNodeCount !== 2 && !ops.allowExplicitVerticalHydrogen)
			return null;

		// sum up, returns successful result
		var result = {
			horizontalSiblings: [nodeSeq[3], nodeSeq[1]],
			verticalSiblings: [nodeSeq[0], nodeSeq[2]]
		};
		if (!ops.reversedDirection)
		{
			result.towardSiblings = result.horizontalSiblings;
			result.awaySiblings = result.verticalSiblings;
		}
		else
		{
			result.towardSiblings = result.verticalSiblings;
			result.awaySiblings = result.horizontalSiblings;
		}
		return result;
	},

	/**
	 * Returns rotation direction of a tetrahedron chiral center. Rotation follows the sequence of param siblings.
	 * @param {Kekule.ChemStructureNode} centerNode Center node used to get center coord. If this param is not set, geometric center
	 *   of sibling nodes will be used instead.
	 * @param {Kekule.ChemStructureNode} refSibling Sibling node to determine the looking direction.
	 * @param {Array} siblings Array of {@link Kekule.ChemStructureNode}, should not include refSibling.
	 * @param {hash} options Calculation option, may include the following fields: <br />
	 *   { <br />
	 *     coordMode: Int, use 2D or 3D coord of nodes. <br />
	 *     withImplicitSibling: Bool, whether there is a implicit sibling (e.g., implicit H atom). Coord of implicit node will be calculated from other sibling nodes.
	 *       If this param is true and param refSibling is null, this implicit node will be regarded as refSibling, otherwise the
	 *       implicit node will be regarded as the last one of siblings. <br />
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Default is true. Only works when coord mode is 2D.
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree).
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure).
	 *       Default is true.
	 *     allowExplicitVerticalHydrogen: Whether the implicit hydrogen on vertical direction is allowed (used in SMILES generation).
	 *   }
	 * @returns {Int} Value from {@link Kekule.RotationDir}, clockwise, anti-clockwise or unknown.
	 */
	calcTetrahedronChiralCenterRotationDirectionEx: function(centerNode, refSibling, siblings, options)
	{
		var ops = Object.extend({
			implicitFischerProjection: true,
			allowExplicitHydrogenInFischer: true
		}, options);

		var coordMode = ops.coordMode;
		var withImplicitSibling = !!ops.withImplicitSibling;
		var refSiblingBehind = !!ops.refSiblingBehind;
		var implicitFischerProjection = !!ops.implicitFischerProjection;

		if (!coordMode)
		{
			var node = centerNode || refSibling || siblings[0];
			if (!node)
				return RD.UNKNOWN;
			if (node.hasCoord3D())
				coordMode = Kekule.CoordMode.COORD3D;  // default use 3D coord to calculate chirality
			else
				coordMode = Kekule.CoordMode.COORD2D;
		}

		var is2D = coordMode === Kekule.CoordMode.COORD2D;

		var explicitSiblingCount = siblings.length;
		var totalSiblingCount = explicitSiblingCount;
		if (refSibling)
			++totalSiblingCount;
		if (withImplicitSibling)
			++totalSiblingCount;

		if (totalSiblingCount < 4)  // not a tetrahedron
			return RD.UNKNOWN;

		var getNodeCoord = function(node, centerNode, centerCoord, coordMode, fischerInfo)
		{
			/*
			 var is2D = coordMode === Kekule.CoordMode.COORD2D;
			 if (is2D && !node.hasCoord2D())
			 coordMode = Kekule.CoordMode.COORD3D;
			 if (!is2D && !node.hasCoord3D())
			 coordMode = Kekule.CoordMode.COORD2D;
			 */
			if (coordMode !== Kekule.CoordMode.COORD2D)  // 3D, get 3D absolute coord directly
				return node.getAbsCoordOfMode(coordMode, true);  // allow borrow
			else  // coord 2D, add z value, consider wedge bonds and special "zIndex2D" property expliciting z stack of 2D sketch
			{
				var result = node.getAbsCoordOfMode(coordMode, true);  // allow borrow
				var defCoordZ;
				if (centerNode && centerCoord)
				{
					defCoordZ = centerCoord.z;
					var connector = node.getConnectorTo(centerNode);
					if (connector.getStereo)
					{
						var connDirection = connector.indexOfConnectedObj(node) - connector.indexOfConnectedObj(centerNode);
						var BS = Kekule.BondStereo;
						var bondStereo = connector.getStereo() || BS.NONE;
						if (bondStereo === BS.NONE && fischerInfo)  // with fischer projection info, there is implicit bond stereo
						{
							if (fischerInfo.towardSiblings.indexOf(node) >= 0)
								bondStereo = (connDirection < 0)? BS.UP_INVERTED: BS.UP;
							else if (fischerInfo.awaySiblings.indexOf(node) >= 0)
								bondStereo = (connDirection < 0)? BS.DOWN_INVERTED: BS.DOWN;
						}
						var wedgeDirs = [BS.UP, BS.UP_INVERTED, BS.DOWN, BS.DOWN_INVERTED];
						if (wedgeDirs.indexOf(bondStereo) >= 0)
						{
							if (connDirection < 0)
								bondStereo = BS.getInvertedDirection(bondStereo);
							var zFactors = [1, -1, -1, 1];
							var distance = CU.getDistance(result, /*centerNode.getAbsCoordOfMode(coordMode)*/centerCoord);
							result.z = distance * zFactors[wedgeDirs.indexOf(bondStereo)];
						}
						else if ([BS.UP_OR_DOWN, BS.UP_OR_DOWN_INVERTED].indexOf(bondStereo) >= 0)  // direction not certain
							return null;  // return a special mark, can determinate angle calculation
					}
				}
				if (Kekule.ObjUtils.isUnset(result.z) && node.getZIndex2D)  // check zIndex2D property of node
				{
					result.z = node.getZIndex2D();
					if (Kekule.ObjUtils.isUnset(result.z))
						result.z = defCoordZ;
				}
				result.z = result.z || 0;
				return result;
			}
		};

		// calc all essetianl coords: centerCoord, coords of rotation siblings, coord of implict node
		var centerCoord = centerNode? getNodeCoord(centerNode, null, null, coordMode): null;
		var fischerOptions = {
			'allowedError': ops.fischerAllowedError,
			'reversedDirection': ops.reversedFischer,
			'allowExplicitHydrogen': ops.allowExplicitHydrogenInFischer,
			'allowExplicitVerticalHydrogen': ops.allowExplicitVerticalHydrogen
		};
		var allAroundSiblings = [].concat(siblings);
		if (refSibling)
			Kekule.ArrayUtils.pushUnique(allAroundSiblings, refSibling);
		var fischerInfo = (centerNode && ops.implicitFischerProjection)? Kekule.MolStereoUtils._getFischerProjectionInfo(centerNode, allAroundSiblings, fischerOptions): null;

		var coords = [];
		for (var i = 0; i < explicitSiblingCount; ++i)
		{
			var coord = getNodeCoord(siblings[i], centerNode, centerCoord, coordMode, fischerInfo);
			coords.push(coord);
		}
		var allExplicitSiblingCoords = AU.clone(coords);
		var refCoord = refSibling ? getNodeCoord(refSibling, centerNode, centerCoord, coordMode, fischerInfo) : null;
		if (refCoord)
			allExplicitSiblingCoords.push(refCoord);

		if (allExplicitSiblingCoords.indexOf(null) >= 0)  // coords has null value (special mark returned by function getNodeCoord, can not calculate
			return RD.UNKNOWN;

		if (!centerCoord)
		{
			var coordSum = {};
			for (var i = 0, l = allExplicitSiblingCoords.length; i < l; ++i)
			{
				coordSum = CU.add(allExplicitSiblingCoords[i], coordSum);
			}
			centerCoord = CU.divide(coordSum, allExplicitSiblingCoords.length);
		}

		// turn all coords to relative one to centerCoord, affect items of both allExplicitSiblingCoords and coords
		var wedgeNodeCount = 0;
		for (var i = 0, l = allExplicitSiblingCoords.length; i < l; ++i)
		{
			var oldCoord = allExplicitSiblingCoords[i];
			var c = CU.substract(oldCoord, centerCoord);
			oldCoord.x = c.x;
			oldCoord.y = c.y;
			oldCoord.z = c.z;
			if (is2D && !!c.z)  // z coord not 0 in 2D mode
				++wedgeNodeCount;
		}
		if (is2D && wedgeNodeCount <= 0)  // in 2D mode but with no wedge bond, can not determine
			return RD.UNKNOWN;

		centerCoord = {'x': 0, 'y': 0, 'z': 0};

		if (withImplicitSibling)  // calc coord of implicit siblings
		{
			var coordSum = {};
			for (var i = 0, l = allExplicitSiblingCoords.length; i < l; ++i)
			{
				coordSum = CU.add(allExplicitSiblingCoords[i], coordSum);
			}
			var implicitCoord = CU.substract({'x': 0, 'y': 0, 'z': 0}, coordSum);
			if (!refSibling)
				refCoord = implicitCoord;
			else
				coords.push(implicitCoord);
		}

		// now we have all essential coords, begin the calculation
		if (coords.length === 3 && coords.indexOf(null) < 0)
			return Kekule.MolStereoUtils.calcRotationDirection(centerCoord, refCoord, coords[0], coords[1], coords[2], refSiblingBehind);
		else
			return RD.UNKNOWN;
	},
	/**
	 * Returns rotation direction of a tetrahedron chiral center. Rotation follows the sequence of param siblings.
	 * @param {Int} coordMode Use 2D or 3D coord of nodes.
	 * @param {Kekule.ChemStructureNode} centerNode Center node used to get center coord. If this param is not set, geometric center
	 *   of sibling nodes will be used instead.
	 * @param {Kekule.ChemStructureNode} refSibling Sibling node to determine the looking direction.
	 * @param {Array} siblings Array of {@link Kekule.ChemStructureNode}, should not include refSibling.
	 * @param {Bool} withImplicitSibling Whether there is a implicit sibling (e.g., implicit H atom). Coord of implicit node will be calculated from other sibling nodes.
	 *   If this param is true and param refSibling is null, this implicit node will be regarded as refSibling, otherwise the
	 *   implicit node will be regarded as the last one of siblings.
	 * @param {Hash} additionalOptions
	 * @param {Bool} refSiblingBehind Whether put refCoord behide center coord.
	 * @returns {Int} Value from {@link Kekule.RotationDir}, clockwise, anti-clockwise or unknown.
	 */
	calcTetrahedronChiralCenterRotationDirection: function(coordMode, centerNode, refSibling, siblings, withImplicitSibling, refSiblingBehind, additionalOptions)
	{
		var ops = Object.create(additionalOptions);
		return Kekule.MolStereoUtils.calcTetrahedronChiralCenterRotationDirectionEx(centerNode, refSibling, siblings, Object.extend(ops, {
			'coordMode': coordMode,
			'withImplicitSibling': withImplicitSibling,
			'refSiblingBehind': refSiblingBehind
		}));
	},

	/**
	 * Calc and returns MDL stereo parity of a chiral node.
	 * Note: the parent structure fragment should be canonicalized.
	 * @param {Kekule.ChemStructureNode} node
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 *
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     ignoreChiralCheck: Bool, Whether bypass the check to ensure node is a chiral center. <br/>
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * //@param {Kekule.StructureFragment} parentMol
	 * @returns {Int} Value from {@link Kekule.StereoParity}.
	 */
	calcTetrahedronChiralNodeParity: function(node, coordMode, options)
	{
		var ops = Object.create(options || null);
		if (!coordMode)
		{
			if (node.hasCoord3D())
				coordMode = Kekule.CoordMode.COORD3D;  // default use 3D coord to calculate chirality
			else
				coordMode = Kekule.CoordMode.COORD2D;
		}
		ops.coordMode = coordMode;
		ops.refSiblingBehind = true;


		var KS = Kekule.StereoParity;
		/*
		if (!parentMol)
			parentMol = node.getParent();
		*/
		var ignoreChiralCheck = ops.ignoreChiralCheck;
		if (!ignoreChiralCheck && !Kekule.MolStereoUtils.isChiralNode(node))
			return KS.NONE;
		var siblings = node.getLinkedChemNodes();
		var hydroCount = node.getHydrogenCount();
		ops.withImplicitSibling = !!hydroCount || (siblings.length < 4);  // S/P, may three sibling with a electron pair
		//var allSiblingCount = siblings.length + hydroCount;
		//var atomicSymbol = node.getSymbol? node.getSymbol(): null;
		siblings.sort(function(a, b){
			return -((a.getCanonicalizationIndex() || 0) - (b.getCanonicalizationIndex() || 0));
		});
		var refSibling = ops.withImplicitSibling? null: siblings[3];
		siblings = siblings.slice(0, 3);
		//var rotationDir = Kekule.MolStereoUtils.calcTetrahedronChiralCenterRotationDirection(coordMode, node, refSibling, siblings, withImplicitNode, true);
		var rotationDir = Kekule.MolStereoUtils.calcTetrahedronChiralCenterRotationDirectionEx(node, refSibling, siblings, ops);
		return (rotationDir === RD.CLOCKWISE)? KS.ODD:
			(rotationDir === RD.ANTICLOCKWISE)? KS.EVEN:
				KS.UNKNOWN;
	},

	/**
	 * Detect and mark parity of all chiral nodes in structure fragment.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization If false, ctab will be canonicalized before perception.
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * @returns {Array} Array of all chiral nodes.
	 * @private
	 */
	doPerceiveChiralNodes: function(structFragmentOrCtab, coordMode, ignoreCanonicalization, options)
	{
		var ops = Object.create(options || null);
		var parityCalcOps = Object.create(ops);
		parityCalcOps.ignoreChiralCheck = true;
		var result = Kekule.MolStereoUtils.doFindChiralNodes(structFragmentOrCtab, ignoreCanonicalization);
		structFragmentOrCtab.beginUpdate();
		try
		{
			if (result && result.length)
			{
				for (var i = 0, l = result.length; i < l; ++i)
				{
					var n = result[i];
					var parity = Kekule.MolStereoUtils.calcTetrahedronChiralNodeParity(n, coordMode, parityCalcOps);
					if (n.setParity)
					{
						//console.log('set parity', n.getId(), parity);
						n.setParity(parity);
					}
				}
			}
		}
		finally
		{
			structFragmentOrCtab.endUpdate();
		}
		return result;
	},

	/**
	 * Detect and mark parity of all chiral nodes/connectors in structure fragment.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization If false, ctab will be canonicalized before perception.
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     useFlatternedShadow: Bool, use flatterned shadow structure to perceive stereo. Default is true. <br />
	 *     perceiveStereoConnectors: Bool, whether find out the all stereo bonds, default is true. <br />
	 *     perceiveChiralNodes: Bool, whether find out all stereo atoms, default is true. <br />
	 *     calcParity: Bool, whether calculate the parity of stereo bonds and node found, default is true. <br />
	 *     strictStereoBondGeometry: Bool, if true, the illegal bond geometry will be ignored in calculation (e.g., two connected atoms on same side of a double bond). <br />
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead. <br />
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * @returns {Array} Array of all nodes and connectors with special stereo parities.
	 */
	perceiveStereos: function(structFragmentOrCtab, coordMode, ignoreCanonicalization, options)
	{
		/*
		var ops = Object.extend({
			useFlatternedShadow: true,
			perceiveStereoConnectors: true,
			perceiveChiralNodes: true,
			calcParity: true
		}, options);
		*/
		var ops = Object.extend(Object.extend({}, Kekule.globalOptions.algorithm.stereoPerception), options);

		var result;

		var srcStructFragment = (structFragmentOrCtab instanceof Kekule.StructureConnectionTable) ? structFragmentOrCtab.getParent() : structFragmentOrCtab;

		var targetFragment;
		if (ops.useFlatternedShadow)
		{
			targetFragment = srcStructFragment.getFlattenedShadowFragment(true);
		}
		else
			targetFragment = srcStructFragment;

		// Canonicalize first
		if (!ignoreCanonicalization)
		{
			Kekule.canonicalizer.canonicalize(targetFragment, 'morganEx');
		}

		targetFragment.beginUpdate();
		try
		{
			// then perceive and calculate stereo
			var stereoBonds, chiralNodes;
			if (ops.perceiveStereoConnectors)
			{
				if (ops.calcParity)
					stereoBonds = Kekule.MolStereoUtils.doPerceiveStereoConnectors(targetFragment, coordMode, true, ops.strictStereoBondGeometry);
				else
					stereoBonds = Kekule.MolStereoUtils.doFindStereoBonds(targetFragment, true);
			}
			if (ops.perceiveChiralNodes)
			{
				if (ops.calcParity)
					chiralNodes = Kekule.MolStereoUtils.doPerceiveChiralNodes(targetFragment, coordMode, true, ops);
				else
					chiralNodes = Kekule.MolStereoUtils.doFindChiralNodes(targetFragment, true);
			}
			var stereoObjs = (chiralNodes || []).concat(stereoBonds || []);
			//console.log(ops, stereoBonds, chiralNodes, stereoObjs);

			if (ops.useFlatternedShadow && !srcStructFragment.getFlattenedShadowOnSelf())  // map back to src fragment
			{
				result = [];
				srcStructFragment.beginUpdate();
				try
				{
					//var shadowInfo = srcStructFragment.getFlattenedShadow();
					for (var i = 0, l = stereoObjs.length; i < l; ++i)
					{
						var srcObj = srcStructFragment.getFlatternedShadowSourceObj(stereoObjs[i]);
						if (srcObj)
						{
							if (ops.calcParity)
								srcObj.setParity(stereoObjs[i].getParity());
							result.push(srcObj);
						}
					}
				}
				finally
				{
					srcStructFragment.endUpdate();
				}
			}
			else
				result = stereoObjs;
		}
		finally
		{
			targetFragment.endUpdate();
		}

		return result;

		/*
		var stereoBonds = Kekule.MolStereoUtils.perceiveStereoConnectors(structFragmentOrCtab, coordMode, ignoreCanonicalization);
		var chiralNodes = Kekule.MolStereoUtils.perceiveChiralNodes(structFragmentOrCtab, coordMode, true, options);  // already canonicalized when finding bonds
		if (stereoBonds)
			result = result.concat(stereoBonds);
		if (chiralNodes)
			result = result.concat(chiralNodes);
		//console.log(result, stereoBonds, chiralNodes);
		return result;
		*/
	},

	/**
	 * Find stereo double bond in struct fragment or ctab.
	 * Note, before finding, if param ignoreCanonicalization is false,
	 * the struct fragment will be canonicalized by morgan algorithm to set node cano index.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Bool} ignoreCanonicalization
	 * @returns {Array}
	 * @deprecated
	 */
	findStereoConnectors: function(structFragmentOrCtab, ignoreCanonicalization)
	{
		return Kekule.MolStereoUtils.perceiveStereos(structFragmentOrCtab, null, ignoreCanonicalization,
				{
					perceiveStereoConnectors: true,
					perceiveChiralNodes: false,
					calcParity: false
				});
	},

	/**
	 * Detect and mark parity of all stereo bonds in structure fragment.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization If false, ctab will be canonicalized before perception.
	 * @param {Bool} strictStereoBondGeometry
	 * @returns {Array} Array of all chiral nodes.
	 * @deprecated
	 */
	perceiveStereoConnectors: function(structFragmentOrCtab, coordMode, ignoreCanonicalization, strictStereoBondGeometry)
	{
		return Kekule.MolStereoUtils.perceiveStereos(structFragmentOrCtab, coordMode, ignoreCanonicalization,
				{
					'perceiveStereoConnectors': true,
					'perceiveChiralNodes': false,
					'calcParity': true,
					'strictStereoBondGeometry': strictStereoBondGeometry
				});
	},

	/**
	 * Find chiral nodes in struct fragment or ctab.
	 * Note, before finding, if param ignoreCanonicalization is false,
	 * the struct fragment will be canonicalized by morgan algorithm to set node cano index.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Bool} ignoreCanonicalization
	 * @returns {Array}
	 * @deprecated
	 */
	findChiralNodes: function(structFragmentOrCtab, ignoreCanonicalization)
	{
		return Kekule.MolStereoUtils.perceiveStereos(structFragmentOrCtab, null, ignoreCanonicalization,
				{
					perceiveStereoConnectors: false,
					perceiveChiralNodes: true,
					calcParity: false
				});
	},

	/**
	 * Detect and mark parity of all chiral nodes in structure fragment.
	 * @param {Variant} structFragmentOrCtab
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization If false, ctab will be canonicalized before perception.
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * @returns {Array} Array of all chiral nodes.
	 * @deprecated
	 */
	perceiveChiralNodes: function(structFragmentOrCtab, coordMode, ignoreCanonicalization, options)
	{
		var ops = Object.create(options || null);
		ops = Object.extend(ops, {
			perceiveStereoConnectors: false,
			perceiveChiralNodes: true,
			calcParity: true
		});
		return Kekule.MolStereoUtils.perceiveStereos(structFragmentOrCtab, coordMode, ignoreCanonicalization, ops);
	}
};

/**
 * An extensive canonicalization indexer class based on Morgan algorithm but consider of stereo.
 * @arguments Kekule.CanonicalizationMorganIndexer
 * @class
 */
Kekule.CanonicalizationMorganExIndexer = Class.create(Kekule.CanonicalizationMorganIndexer,
/** @lends Kekule.CanonicalizationMorganExIndexer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.CanonicalizationMorganExIndexer',
	/** @ignore */
	doExecute: function($super, ctab)
	{
		// do a normal morgan indexer first
		$super(ctab);
		//var nodes = ctab.getNodes();
		var nodes = ctab.getNonHydrogenNodes();
		var sortedNodes = this._groupNodesByCanoIndex(nodes);
		// then detect stereo factors based on indexes
		var stereoObjs = null;
		var stereoObjCount = 0;
		stereoObjs = Kekule.MolStereoUtils.perceiveStereos(ctab, null, true) || [];  // do not canonicalize again
		while (stereoObjCount < stereoObjs.length)  // repeat until no more stereo objects is found
		{
			// if new stereo objects is found, reindex nodes via stereo information
			sortedNodes = this._regroupSortedNodes(sortedNodes);
			this._setCanonicalizationIndexToNodeGroups(sortedNodes);

			stereoObjCount = stereoObjs.length;
			stereoObjs = Kekule.MolStereoUtils.perceiveStereos(ctab, null, true) || [];
			//console.log('here', stereoObjCount, stereoObjs.length);
		}
	},

	/** @private */
	_groupNodesByCanoIndex: function(nodes)
	{
		return AU.group(nodes, function(a, b) {
			return (a.getCanonicalizationIndex() || -1) - (b.getCanonicalizationIndex() || -1);
		});
	},
	/** @private */
	_regroupSortedNodes: function(sortedNodes)
	{
		var result = [];
		for (var i = 0, l = sortedNodes.length; i < l; ++i)
		{
			var n = sortedNodes[i];
			if (!AU.isArray(n))
			{
				result.push(n);
			}
			else
			{
				/*
				var getConnectorCompareValues = function(node)
				{
					var result = [];
					for (var i = 0, l = node.getLinkedConnectorCount(); i < l; ++i)
					{
						var conn = node.getLinkedConnectorAt(i);
						result.push(Kekule.UnivChemStructObjComparer.getCompareValue(conn));
					}
					result.sort(function(a, b) { return a - b; });
					return result;
				};
				*/
				var resorted = AU.group(n, function(a, b) {
					//var result = Kekule.UnivChemStructObjComparer.compare(a, b);
					var result = a.compareStructure(b);
					if (result === 0)  // still can not distinguish, check linked bonds
					{
						/*
						var cvaluesA = getConnectorCompareValues(a);
						var cvaluesB = getConnectorCompareValues(b);
						result = AU.compare(cvaluesA, cvaluesB);
            */

						var connsA = a.getLinkedConnectors();
						var connsB = b.getLinkedConnectors();
						result = Kekule.ObjComparer.compareStructure(connsA, connsB);
					}
					return result;
				});
				result = result.concat(resorted);
			}
		}
		return result;
	}
});
// register morganEx canonicalizer, and as default
Kekule.canonicalizer.registerExecutor('morganEx', [Kekule.CanonicalizationMorganExIndexer, Kekule.CanonicalizationMorganNodeSorter], true);

/** @ignore */
ClassEx.extend(Kekule.StructureFragment,
/** @lends Kekule.StructureFragment# */
{
	/**
	 * Detect and mark parity of all chiral nodes in structure fragment.
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization: Bool. If false, ctab will be canonicalized before perception. <br/>
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * @returns {Array} Array of all chiral nodes.
	 */
	perceiveChiralNodes: function(coordMode, ignoreCanonicalization, options)
	{
		return Kekule.MolStereoUtils.perceiveChiralNodes(this, coordMode, ignoreCanonicalization, options);
	},
	/**
	 * Detect and mark parity of all stereo connectors in structure fragment.
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @returns {Array} Array of all chiral nodes.
	 */
	perceiveStereoConnectors: function(coordMode, ignoreCanonicalization)
	{
		return Kekule.MolStereoUtils.perceiveStereoConnectors(this, coordMode, ignoreCanonicalization);
	},
	/**
	 * Detect and mark parity of all chiral nodes/connectors in structure fragment.
	 * @param {Int} coordMode Use 2D or 3D coord to calculate.
	 * @param {Bool} ignoreCanonicalization: Bool. If false, ctab will be canonicalized before perception. <br/>
	 * @param {Hash} options Chiral calculation options, including:
	 *   { <br/>
	 *     implicitFischerProjection: Bool, whether the "+" cross of Fischer projection need to be recognized and take into consideration.
	 *       Only works when coord mode is 2D. <br/>
	 *     fischerAllowedError: the allowed error when checking vertical and horizontal line in Fischer projection cross,
	 *       default is 0.08 (deltaY/deltaX or vice versa, about 4.5 degree). <br/>
	 *     reversedFischer: If true, the node on vertical line will be toward observer instead,
	 *     allowExplicitHydrogenInFischer: Whether the simplification Fischer projection in saccharide chain form is allowed (H is omitted from structure). <br/>
	 *   }
	 * @returns {Array} Array of all nodes and connectors with special stereo parities.
	 */
	perceiveStereos: function(coordMode, ignoreCanonicalization, options)
	{
		return Kekule.MolStereoUtils.perceiveStereos(this, coordMode, ignoreCanonicalization, options);
	}
});

})();