Source: algorithm/kekule.structures.searches.js

/**
 * @fileoverview
 * Methods about basic chem structures searching and matching.
 * @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.canonicalizers.js
 */

(function(){
"use strict";

var AU = Kekule.ArrayUtils;
//var SC = Kekule.UnivChemStructObjComparer;

/*
 * Default options to do sub structure search.
 * @object
 */
Kekule.globalOptions.add('algorithm.structureSearch', {
	doStandardize: true
});

/**
 * A util class to search sub structures in ctab based molecule.
 * @class
 */
Kekule.ChemStructureSearcher = {
	/* @private */
	//OPS_SKELETAL: {'compareAtom': false, 'compareCharge': false, 'compareBondOrder': false, 'compareStereo': false, 'compareHydrogenCount': false},
	/* @private */
	//OPS_CONSTITUTION: {'compareStereo': false},
	/* @private */
	//OPS_CONFIGRATION: {'compareStereo': true},
	/**
	 * Search sub structure in molecule.
	 * @param {Kekule.StructureFragment} subStructure Structure to find.
	 * @param {Kekule.StructureFragment} sourceMol
	 * @param {Hash} options Search options, can include the following fields:
	 *   {
	 *     doStandardize: Bool, whether standardize molecule (especially perceive aromatic rings) before searching,
	 *       default is true.
	 *     exactMatch: Bool, if true, only the same structure with subStructure will be matched.
	 *     structureLevel: comparation level, value from {@link Kekule.StructureComparationLevel}. Default is constitution.
	 *     atom: Bool, if false, all node will be regarded as same. Default is true.
	 *     mass: Bool, whether mass number is compared. Default is false.
	 *     charge: Bool, whether charge of node is compared. Default is false.
	 *     bondOrder: Bool, whether order of bond is compared. Default is true.
	 *     stereo: Bool, whether stereo feature (chiral center, cis/trans of double bond) is taken into consideration. Default is false.
	 *   }
	 * @returns {Variant} If sub structure is found, an array of matching node and connectors will be returned.
	 *   Otherwise false will be returned.
	 */
	findSubStructure: function(subStructure, sourceMol, options)
	{
		// TODO: configuration search need to be rechecked
		var op = Object.extend(Object.extend({}, Kekule.globalOptions.algorithm.structureSearch), options);

		if (op.exactMatch)
		{
			var matched = sourceMol.isSameStructureWith(subStructure, op);
			if (matched)
				return [].concat(sourceMol.getNodes()).concat(sourceMol.getConnectors());
			else
				return false;
		}

		/*
		var KS = Kekule.ChemStructureSearcher;
		var initialCompareOptions = op.compareConfiguration? KS.OPS_CONFIGRATION:
			op.compareConstitution? KS.OPS_CONSTITUTION:
				op.compareSkeletal? KS.OPS_SKELETAL:
				{};

		op = Object.extend(op, initialCompareOptions);

		var objCompareOptions = Object.create({
			compareAtom: op.compareAtom,
			compareMass: op.compareMass,
			compareStereo: op.compareStereo,
			compareLinkedConnectorCount: false,
			compareCharge: op.compareCharge,
			compareHydrogenCount: false,
			compareConnectedObjCount: false,
			compareBondOrder: op.compareBondOrder
		});
		*/
		// initial options
		op = Object.extend({level: Kekule.StructureComparationLevel.CONSTITUTION,
			compareLinkedConnectorCount: false, compareHydrogenCount: false, compareConnectedObjCount: false}, op);
		/*
		var op1 = Kekule.UnivChemStructObjComparer.prepareCompareOptions(op);
		var op2 = Kekule.ObjComparer.getStructureComparisonDetailOptions(op);
		op = op2;
		console.log(op1);
		console.log(op2);
		console.log('===================');
		*/
		op = Kekule.ObjComparer.getStructureComparisonDetailOptions(op);
		var objCompareOptions = op;

		// standardize structures first, perceive aromatic rings
		//console.log(op);
		if (op.doStandardize)
		{
			var standardizeOps = {
				unmarshalSubFragments: !true,
				doCanonicalization: true,
				doAromaticPerception: true
			};
			if (!op.compareStereo)  // do not need perceive stereo
				standardizeOps.doCanonicalization = false;
			// here we ensure two structures are standardized (with their flattened shadows).
			subStructure.standardize(standardizeOps);
			sourceMol.standardize(standardizeOps);
		}
		var flattenedSrcMol = sourceMol.getFlattenedShadowFragment(true);
		var flattenedSubStruct = subStructure.getFlattenedShadowFragment(true);

		/*
		var targetStartingNode = subStructure.getNonHydrogenNodes()[0]; //subStructure.getNodeAt(0);
		var srcNodes = sourceMol.getNonHydrogenNodes(); //sourceMol.getNodes();
		*/
		var targetStartingNode = flattenedSubStruct.getNonHydrogenNodes()[0]; //subStructure.getNodeAt(0);
		var srcNodes = flattenedSrcMol.getNonHydrogenNodes(); //sourceMol.getNodes();

		var srcNodeCount = srcNodes.length;
		var srcIndex = 0;

		var initComparedTargetObjs = [];
		var initComparedSrcObjs = [];

		//console.log(options, objCompareOptions);

		var compareResult = false;
		while (!compareResult && (srcIndex < srcNodeCount))
		{
			compareResult = SM._compareNode(targetStartingNode, srcNodes[srcIndex], initComparedTargetObjs, initComparedSrcObjs, objCompareOptions);
			++srcIndex;
		}

		//if (srcIndex >= srcNodeCount)  // can not find
		if (!compareResult)
			return false;
		else
		{
			var matchedObjs = AU.toUnique(compareResult);
			// map back to original structure
			var result = sourceMol.getFlatternedShadowSourceObjs(matchedObjs);
			return result;
		}
	},

	/*
	__reportComparedObjsChange: function(objs, tag)
	{
		var nodeIds = [];
		for (var i = 0, l = objs.length; i < l; ++i)
		{
			var o = objs[i];
			if (o.getId)
				nodeIds.push(o.getId());
		}
		var s = '[' + nodeIds.join(', ') + ']';
		if (tag)
			s = '(' + tag + ') ' + s;
		console.log(s);
	},
	*/

	/** @private */
	_addToComparedObjs: function(obj, comparedObjs, tag)
	{
		/*
		if (comparedObjs.indexOf(obj) >= 0)
			console.log('dup', tag, obj.getId());
		*/
		//AU.pushUnique(comparedObjs, obj);
		comparedObjs.push(obj);
		//obj.setRenderOption('color', 'red');

		//SM.__reportComparedObjsChange(comparedObjs, tag);
	},
	/** @private */
	_removeFromComparedObjs: function(obj, comparedObjs, tag)
	{
		//AU.remove(comparedObjs, obj);
		var index = comparedObjs.indexOf(obj);
		comparedObjs.length = index;
		//obj.setRenderOption('color', null);

		//SM.__reportComparedObjsChange(comparedObjs, tag);
	},
	/** @private */
	_isSameMappedObj: function(targetObj, srcObj, comparedTargetObjs, comparedSrcObjs)
	{
		var srcIndex = comparedSrcObjs.indexOf(srcObj);
		var targetIndex = comparedTargetObjs.indexOf(targetObj);
		return (srcIndex >= 0) && (srcIndex === targetIndex);
	},
	/** @private */
	_compareComparedNeighbors: function(targetObjs, srcObjs, allComparedTargetObjs, allComparedSrcObjs)
	{
		if (targetObjs.length > srcObjs.length)
			return false;

		var targetIndexes = [];
		for (var i = 0, l = targetObjs.length; i < l; ++i)
		{
			targetIndexes.push(allComparedTargetObjs.indexOf(targetObjs[i]));
		}
		//targetIndexes.sort();
		var srcIndexes = [];
		for (var i = 0, l = srcObjs.length; i < l; ++i)
		{
			srcIndexes.push(allComparedSrcObjs.indexOf(srcObjs[i]));
		}
		//srcIndexes.sort();

		for (var i = 0, l = targetIndexes.length; i < l; ++i)
		{
			if (srcIndexes.indexOf(targetIndexes[i]) < 0)
				return false;
		}
		return true;
	},

	/** @private */
	_compareNode: function(targetNode, srcNode, comparedTargetObjs, comparedSrcObjs, compareOptions)
	{
		/*
		if (SM._isSameMappedObj(targetNode, srcNode, comparedTargetObjs, comparedSrcObjs))
			return true;

		var compareOptions = {
			compareLinkedConnectorCount: false,
			compareCharge: false,
			compareHydrogenCount: false
		};
		*/

		//if (SC.compare(targetNode, srcNode, compareOptions) !== 0)
		if (targetNode.compareStructure(srcNode, compareOptions) !== 0)
			return false;
		else
		{
			var targetConnectors = targetNode.getLinkedNonHydrogenConnectors(); // AU.clone(targetNode.getLinkedConnectors());
			var srcConnectors = srcNode.getLinkedNonHydrogenConnectors(); //AU.clone(srcNode.getLinkedConnectors());

			var targetConnectorCount = targetConnectors.length;  //targetNode.getLinkedConnectorCount();
			var srcConnectorCount = srcConnectors.length; //srcNode.getLinkedConnectorCount();
			if (targetConnectorCount > srcConnectorCount)
				return false;

			var dupComparedTargetObjs = AU.clone(comparedTargetObjs);
			var dupComparedSrcObjs = AU.clone(comparedSrcObjs);
			SM._addToComparedObjs(targetNode, dupComparedTargetObjs, 'target');
			SM._addToComparedObjs(srcNode, dupComparedSrcObjs, 'src');
			/*
			SM._addToComparedObjs(targetNode, comparedTargetObjs, 'target');
			SM._addToComparedObjs(srcNode, comparedSrcObjs, 'src');
			*/

			/*
			var targetConnectors = AU.exclude(AU.clone(targetNode.getLinkedConnectors()), comparedTargetObjs);
			var srcConnectors = AU.exclude(AU.clone(srcNode.getLinkedConnectors()), comparedSrcObjs);
			*/

			var targetComparedConnectors = AU.intersect(targetConnectors, dupComparedTargetObjs);
			var srcComparedConnectors = AU.intersect(srcConnectors, dupComparedSrcObjs);

			var result = true;
			if (!SM._compareComparedNeighbors(targetComparedConnectors, srcComparedConnectors, dupComparedTargetObjs, dupComparedSrcObjs))
				result = false;
			else
			{
				targetConnectors = AU.exclude(targetConnectors, targetComparedConnectors);
				srcConnectors = AU.exclude(srcConnectors, srcComparedConnectors);
				if (targetConnectors.length > srcConnectors.length)
					result = false;
			}

			var nextComparedObjs = [];
			while (result && targetConnectors.length)
			{
				var compareResult = false;
				var targetConn = targetConnectors.shift();
				var srcIndex = 0;
				var srcConn;
				while (srcConnectors.length && !compareResult && (srcIndex < srcConnectors.length))
				{
					srcConn = srcConnectors[srcIndex];
					compareResult = SM._compareConnector(targetConn, srcConn, dupComparedTargetObjs, dupComparedSrcObjs, compareOptions);
					if (compareResult)
						nextComparedObjs = nextComparedObjs.concat(compareResult);
					++srcIndex;
				}
				if (!compareResult)  // can not find match connector
				{
					result = false;
					break;
				}
				else
					AU.remove(srcConnectors, srcConn);
			}

			/*
			if (!result)  // remove targetConn and srcConn from compared
			{
				SM._removeFromComparedObjs(targetNode, comparedTargetObjs, 'target');
				SM._removeFromComparedObjs(srcNode, comparedSrcObjs, 'src');
			}
			*/

			if (result)  // compare success, returns all compared objects
				result = [srcNode].concat(nextComparedObjs) //dupComparedSrcObjs;
			return result;
		}
	},

	/** @private */
	_compareConnector: function(targetConn, srcConn, comparedTargetObjs, comparedSrcObjs, compareOptions)
	{
		// targetConn and srcConn are surely not connected to hydrogen atom
		/*
		if (SM._isSameMappedObj(targetConn, srcConn, comparedTargetObjs, comparedSrcObjs))
			return true;

		var compareOptions = {
			compareConnectedObjCount: false
		};
		*/

		//if (SC.compare(targetConn, srcConn, compareOptions) !== 0)
		if (targetConn.compareStructure(srcConn, compareOptions) !== 0)
			return false;
		else
		{
			var targetConnectedObjCount = targetConn.getConnectedObjCount();
			var srcConnectedObjCount = srcConn.getConnectedObjCount();
			if (targetConnectedObjCount > srcConnectedObjCount)
				return false;

			var dupComparedTargetObjs = AU.clone(comparedTargetObjs);
			var dupComparedSrcObjs = AU.clone(comparedSrcObjs);
			SM._addToComparedObjs(targetConn, dupComparedTargetObjs, 'target');
			SM._addToComparedObjs(srcConn, dupComparedSrcObjs, 'src');
			/*
			SM._addToComparedObjs(targetConn, comparedTargetObjs, 'target');
			SM._addToComparedObjs(srcConn, comparedSrcObjs, 'src');
			*/

			/*
			var targetConnectedObjs = AU.exclude(AU.clone(targetConn.getConnectedObjs()), comparedTargetObjs);
			var srcConnectedObjs = AU.exclude(AU.clone(srcConn.getConnectedObjs()), comparedSrcObjs);
			*/
			var targetConnectedObjs = AU.clone(targetConn.getConnectedObjs());
			var srcConnectedObjs = AU.clone(srcConn.getConnectedObjs());

			var targetComparedObjs = AU.intersect(targetConnectedObjs, dupComparedTargetObjs);
			var srcComparedObjs = AU.intersect(srcConnectedObjs, dupComparedSrcObjs);

			var result = true;
			if (!SM._compareComparedNeighbors(targetComparedObjs, srcComparedObjs, dupComparedTargetObjs, dupComparedSrcObjs))
				result = false;
			else
			{
				targetConnectedObjs = AU.exclude(targetConnectedObjs, targetComparedObjs);
				srcConnectedObjs = AU.exclude(srcConnectedObjs, srcComparedObjs);
				if (targetConnectedObjs.length > srcConnectedObjs.length)
					result = false;
			}

			var nextComparedObjs = [];
			while (result && targetConnectedObjs.length)
			{
				var compareResult = false;
				var targetObj = targetConnectedObjs.shift();
				var srcIndex = 0;
				var srcObj;
				while (srcConnectedObjs.length && !compareResult && (srcIndex < srcConnectedObjs.length))
				{
					srcObj = srcConnectedObjs[srcIndex];
					if (srcObj.getClass() !== targetObj.getClass())
						compareResult = false;
					else if (srcObj instanceof Kekule.ChemStructureNode)
						compareResult = SM._compareNode(targetObj, srcObj, dupComparedTargetObjs, dupComparedSrcObjs, compareOptions);
					else if (srcObj instanceof Kekule.ChemStructureConnector)
						compareResult = SM._compareConnector(targetObj, srcObj, dupComparedTargetObjs, dupComparedSrcObjs, compareOptions);
					else
						compareResult = false;

					if (compareResult)
						nextComparedObjs = nextComparedObjs.concat(compareResult);

					++srcIndex;
				}
				if (!compareResult)  // can not find match connector
				{
					result = false;
					break;
				}
				else
					AU.remove(srcConnectedObjs, srcObj);
			}

			/*
			if (!result)  // remove targetConn and srcConn from compared
			{
				SM._removeFromComparedObjs(targetConn, comparedTargetObjs, 'target');
				SM._removeFromComparedObjs(srcConn, comparedSrcObjs, 'src');
			}
			*/

			if (result)  // compare success, returns all compared objects
				result = [srcConn].concat(nextComparedObjs);  //dupComparedSrcObjs;

			return result;
		}
	}
};

var SM = Kekule.ChemStructureSearcher;

ClassEx.extend(Kekule.StructureFragment,
/** @lends Kekule.StructureFragment# */
{
	/**
	 * Search sub structure in this fragment.
	 * @param {Kekule.StructureFragment} subStructure Structure to find.
	 * @param {Hash} options Search options, can include the following fields:
	 *   {
	 *     doStandardize: Bool, whether standardize molecule (especially perceive aromatic rings) before searching.
	 *     exactMatch: Bool, if true, only the same structure with subStructure will be matched.
	 *     ignoreCharge: Bool
	 *     ignoreBondOrder: Bool
	 *   }
	 * @returns {Variant} If sub structure is found, an array of matching node and connectors will be returned.
	 *   Otherwise false will be returned.
	 */
	search: function(subStructure, options)
	{
		return SM.findSubStructure(subStructure, this, options);
	}
});

})();