kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,563 lines (1,508 loc) • 271 kB
JavaScript
/**
* @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; }
/*
'getter': function() {
var detail = this.getChargeDetail();
return detail? (detail.precise || 0): 0;
},
'setter': function(value) {
var detail = this.getChargeDetail();
if (detail)
detail.precise = value;
else
this.setChargeDetail({precise: value});
}
*/
});
this.defineProp('electronicBias', {'dataType': DataType.INT,
/*
'getter': function() {
var detail = this.getChargeDetail() || {};
return detail.schematic;
},
'setter': function(value) {
var schematicValue;
if (!value)
schematicValue = undefined;
else
schematicValue = Math.round(value) || 0;
var detail = this.getChargeDetail();
if (detail)
detail.schematic = schematicValue;
else
this.setChargeDetail({schematic: schematicValue});
}
*/
});
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);
}
}
}
});
// private, save the precise charge (including particle charge like +0.5) or electronic bias charge (δ+/-),
// the value of this property is a object {precise: float, schematic: int(abs value means the number of + or - mark)}.
this.defineProp('chargeEx', {'dataType': DataType.OBJECT, 'scope': Class.PropertyScope.PRIVATE, 'serializable': false,
'getter': function() {
return {charge: this.getCharge(), electronicBias: this.getElectronicBias()}
},
'setter': function(value) {
this.beginUpdate();
try
{
if (value && Kekule.ObjUtils.notUnset(value.charge))
this.setCharge(value.charge);
if (value && Kekule.ObjUtils.notUnset(value.electronicBias))
this.setElectronicBias(value.electronicBias);
}
finally
{
this.endUpdate();
}
}
});
},
/** @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;
},
/**
* Returns whether the node has an explicit charge setting.
* @returns {Bool}
*/
hasExplicitChargeOrElectronicBias: function() {
return !!(this.getCharge() || this.getElectronicBias());
},
/** @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 */
_getCurrCovalentBond