UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,563 lines (1,508 loc) 271 kB
/** * @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