UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,577 lines (1,521 loc) 75.7 kB
/** * @fileoverview * Operations need to implement an editor. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /widgets/operation/kekule.operations.js * requires /core/kekule.structures.js * requires /widgets/chem/editor/kekule.chemEditor.baseEditors.js * requires /localization/ */ (function(){ "use strict"; var AU = Kekule.ArrayUtils; /** * A namespace for operation about normal ChemObject instance. * @namespace */ Kekule.ChemObjOperation = {}; /** * Base operation for ChemObject instance. * @class * @augments Kekule.Operation * * @param {Kekule.ChemObject} chemObject Target chem object. * * @property {Kekule.ChemObject} target Target chem object. * @property {Bool} allowCoordBorrow Whether allow borrowing between 2D and 3D when manipulating coords. * @property {Kekule.Editor.BaseEditor} The editor object associated. */ Kekule.ChemObjOperation.Base = Class.create(Kekule.Operation, /** @lends Kekule.ChemObjOperation.Base# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.Base', /** @constructs */ initialize: function(/*$super, */chemObj, editor) { this.tryApplySuper('initialize') /* $super() */; this.setTarget(chemObj); if (editor) this.setEditor(editor); }, /** @private */ initProperties: function() { this.defineProp('target', {'dataType': 'Kekule.ChemObject', 'serializable': false}); this.defineProp('allowCoordBorrow', {'dataType': DataType.BOOL}); this.defineProp('editor', {'dataType': 'Kekule.Editor.BaseEditor', 'serializable': false}); }, // A series of notification method to target object /** @private */ notifyBeforeAddingByEditor: function(obj, parent, refSibling) { if (obj.beforeAddingByEditor) // if this special notification method exists, call it first obj.beforeAddingByEditor(parent, refSibling); }, /** @private */ notifyBeforeRemovingByEditor: function(obj, parent) { if (obj.beforeRemovingByEditor) // if this special notification method exists, call it first obj.beforeRemovingByEditor(parent); }, /** @private */ notifyBeforeModifyingByEditor: function(obj, propValues) { if (obj.beforeModifyingByEditor) // if this special notification method exists, call it first obj.beforeModifyingByEditor(propValues); }, /** @private */ notifyAfterAddingByEditor: function(obj, parent, refSibling) { if (obj.afterAddingByEditor) // if this special notification method exists, call it first obj.afterAddingByEditor(parent, refSibling); }, /** @private */ notifyAfterRemovingByEditor: function(obj, parent) { if (obj.afterRemovingByEditor) // if this special notification method exists, call it first obj.afterRemovingByEditor(parent); }, /** @private */ notifyAfterModifyingByEditor: function(obj, propValues) { if (obj.afterModifyingByEditor) // if this special notification method exists, call it first obj.afterModifyingByEditor(propValues); } }); /** * A hack operation of changing a chemObject's class. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Class} newClass * * @property {Class} newClass */ Kekule.ChemObjOperation.ChangeClass = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.ChangeClass# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.ChangeClass', /** @constructs */ initialize: function(/*$super, */chemObj, newClass, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; if (newClass) this.setNewClass(newClass); }, /** @private */ initProperties: function() { this.defineProp('newClass', {'dataType': DataType.CLASS, 'serializable': false}); this.defineProp('oldClass', {'dataType': DataType.CLASS, 'serializable': false}); }, /** @private */ doExecute: function() { var obj = this.getTarget(); obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, {}); if (!this.getOldClass()) { this.setOldClass(obj.getClass()); } obj.__changeClass__(this.getNewClass()); this.notifyAfterModifyingByEditor(obj, {}); } finally { obj.endUpdate(); } }, /** @private */ doReverse: function() { var obj = this.getTarget(); obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, {}); obj.__changeClass__(this.getOldClass()); this.notifyAfterModifyingByEditor(obj, {}); } finally { obj.endUpdate(); } } }); /** * Operation of changing a chemObject's properties. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Hash} newPropValues A hash of new prop-value map. * * @property {Hash} newPropValues A hash of new prop-value map. */ Kekule.ChemObjOperation.Modify = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.Modify# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.Modify', /** @constructs */ initialize: function(/*$super, */chemObj, newPropValues, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; if (newPropValues) this.setNewPropValues(newPropValues); }, /** @private */ initProperties: function() { this.defineProp('newPropValues', {'dataType': DataType.HASH}); this.defineProp('oldPropValues', {'dataType': DataType.HASH}); // private }, /** @private */ doExecute: function() { var oldValues = {}; var map = this.getNewPropValues(); var obj = this.getTarget(); obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, map); for (var prop in map) { var value = map[prop]; // store old value first oldValues[prop] = obj.getPropValue(prop); // set new value obj.setPropValue(prop, value); } this.notifyAfterModifyingByEditor(obj, map); } finally { obj.endUpdate(); } this.setOldPropValues(oldValues); }, /** @private */ doReverse: function() { var map = this.getOldPropValues(); var obj = this.getTarget(); obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, map); for (var prop in map) { var value = map[prop]; // restore old value obj.setPropValue(prop, value); } this.notifyAfterModifyingByEditor(obj, map); } finally { obj.endUpdate(); } } }); /** * Operation of changing a chemObject's hash based properties. * Note that different from {@link Kekule.ChemObjOperation.Modify}, * only the fields existing in new value will be overwrited, other field in old value will remains intact. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {String} propName * @param {Hash} newPropValue New prop hash value. * * @param {String} propName * @property {Hash} newPropValue New prop hash value. */ Kekule.ChemObjOperation.ModifyHashProp = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.ModifyHashProp# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.ModifyHashProp', /** @constructs */ initialize: function(/*$super, */chemObj, propName, newPropValue, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; if (propName) this.setPropName(propName); if (newPropValue) this.setNewPropValue(newPropValue); }, /** @private */ initProperties: function() { this.defineProp('propName', {'dataType': DataType.STRING}); this.defineProp('newPropValue', {'dataType': DataType.HASH}); this.defineProp('oldPropValue', {'dataType': DataType.HASH}); // private }, /** @private */ _extendHash: function(oldValue, newValue) { //var propNames = Kekule.ObjUtils.getOwnedFieldNames(newValue); }, /** @private */ doExecute: function() { var obj = this.getTarget(); var propName = this.getPropName(); var oldValue = obj.getPropValue(this.getPropName()); var newValue = Object.extend(Object.extend({}, oldValue), this.getNewPropValue(), !true); var valueMap = {}; valueMap[propName] = newValue; obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, valueMap); obj.setPropValue(propName, newValue); this.notifyAfterModifyingByEditor(obj, valueMap); } finally { obj.endUpdate(); } if (!this.getOldPropValue()) this.setOldPropValue(oldValue); }, /** @private */ doReverse: function() { var obj = this.getTarget(); var propName = this.getPropName(); var oldValue = this.getOldPropValue(); var nowValue = obj.getPropValue(propName); //var reverseValue = Object.extend(Object.extend({}, nowValue), oldValue, !true); var reverseValue = oldValue; var valueMap = {}; valueMap[propName] = reverseValue; obj.beginUpdate(); try { this.notifyBeforeModifyingByEditor(obj, valueMap); obj.setPropValue(propName, reverseValue); this.notifyAfterModifyingByEditor(obj, valueMap); } finally { obj.endUpdate(); } } }); /** * Operation of changing a chemObject's coord. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Hash} newCoord * @param {Int} coordMode * @param {Bool} useAbsBaseCoord * * @property {Hash} newCoord * @property {Hash} oldCoord If old coord is not set, this property will be automatically calculated when execute the operation. * @property {Int} coordMode * @property {Bool} useAbsBaseCoord */ Kekule.ChemObjOperation.MoveTo = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.MoveTo# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.MoveTo', /** @constructs */ initialize: function(/*$super, */chemObj, newCoord, coordMode, useAbsBaseCoord, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; if (newCoord) this.setNewCoord(newCoord); this.setCoordMode(coordMode || Kekule.CoordMode.COORD2D); this.setUseAbsBaseCoord(!!useAbsBaseCoord); }, /** @private */ initProperties: function() { this.defineProp('newCoord', {'dataType': DataType.HASH}); this.defineProp('oldCoord', {'dataType': DataType.HASH}); this.defineProp('coordMode', {'dataType': DataType.INT}); this.defineProp('useAbsBaseCoord', {'dataType': DataType.BOOL}); }, /** @private */ setObjCoord: function(obj, coord, coordMode) { if (obj && coord && coordMode) { var success = false; if (this.getUseAbsBaseCoord()) { /* if (obj.setAbsCoordOfMode) { obj.setAbsCoordOfMode(coord, coordMode); success = true; } */ if (obj.setAbsBaseCoord) { // console.log('set coord', obj.id, coord); obj.setAbsBaseCoord(coord, coordMode, this.getAllowCoordBorrow()); success = true; } } else { if (obj.setCoordOfMode) { obj.setCoordOfMode(coord, coordMode); success = true; } } if (!success) { var className = obj.getClassName? obj.getClassName(): (typeof obj); Kekule.warn(/*Kekule.ErrorMsg.CAN_NOT_SET_COORD_OF_CLASS*/Kekule.$L('ErrorMsg.CAN_NOT_SET_COORD_OF_CLASS').format(className)); } } }, /** @private */ getObjCoord: function(obj, coordMode) { if (this.getUseAbsBaseCoord()) { /* if (obj.getAbsCoordOfMode) return obj.getAbsCoordOfMode(coordMode, this.getAllowCoordBorrow()); */ if (obj.getAbsBaseCoord) return obj.getAbsBaseCoord(coordMode, this.getAllowCoordBorrow()); } else { if (obj.getCoordOfMode) return obj.getCoordOfMode(coordMode, this.getAllowCoordBorrow()); } return null; }, /** @private */ doExecute: function() { var obj = this.getTarget(); if (!this.getOldCoord()) this.setOldCoord(this.getObjCoord(obj, this.getCoordMode())); if (this.getNewCoord()) this.setObjCoord(this.getTarget(), this.getNewCoord(), this.getCoordMode()); }, /** @private */ doReverse: function() { if (this.getOldCoord()) { this.setObjCoord(this.getTarget(), this.getOldCoord(), this.getCoordMode()); } } }); /** * Operation of changing a chem object's size and coord. * @class * @augments Kekule.ChemObjOperation.MoveTo * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Hash} newDimension {width, height} * @param {Hash} newCoord * @param {Int} coordMode * @param {Bool} useAbsCoord * * @property {Hash} newDimension * @property {Hash} oldDimension If old dimension is not set, this property will be automatically calculated when execute the operation. */ Kekule.ChemObjOperation.MoveAndResize = Class.create(Kekule.ChemObjOperation.MoveTo, /** @lends Kekule.ChemObjOperation.MoveAndResize# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.MoveAndResize', /** @constructs */ initialize: function(/*$super, */chemObj, newDimension, newCoord, coordMode, useAbsCoord, editor) { this.tryApplySuper('initialize', [chemObj, newCoord, coordMode, useAbsCoord, editor]) /* $super(chemObj, newCoord, coordMode, useAbsCoord, editor) */; }, /** @private */ initProperties: function() { this.defineProp('newDimension', {'dataType': DataType.HASH}); this.defineProp('oldDimension', {'dataType': DataType.HASH}); }, /** @private */ setObjSize: function(obj, dimension, coordMode) { if (obj && dimension) { if (obj.setSizeOfMode) { obj.setSizeOfMode(dimension, coordMode); } else { var className = obj.getClassName? obj.getClassName(): (typeof obj); Kekule.warn(/*Kekule.ErrorMsg.CAN_NOT_SET_DIMENSION_OF_CLASS*/Kekule.$L('ErrorMsg.CAN_NOT_SET_DIMENSION_OF_CLASS').format(className)); } } }, /** @private */ getObjSize: function(obj, coordMode) { if (obj.getSizeOfMode) return obj.getSizeOfMode(coordMode, this.getAllowCoordBorrow()); else return null; }, /** @private */ doExecute: function(/*$super*/) { this.tryApplySuper('doExecute') /* $super() */; var obj = this.getTarget(); if (!this.getOldDimension()) { this.setOldDimension(this.getObjSize(obj, this.getCoordMode())); } if (this.getNewDimension()) this.setObjSize(this.getTarget(), this.getNewDimension(), this.getCoordMode()); }, /** @private */ doReverse: function(/*$super*/) { if (this.getOldDimension()) this.setObjSize(this.getTarget(), this.getOldDimension(), this.getCoordMode()); this.tryApplySuper('doReverse') /* $super() */; } }); /** * Operation of changing a chemObject's coord. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Array} objCoordAndSizeInfo * @param {Int} coordMode * @param {Bool} useAbsCoord * * @param {Array} objCoordAndSizeInfo A array of all moved/resized objects infos. Each item should include fields: {obj, oldCoord, newCoord, oldDimension, newDimension} * @param {Int} coordMode * @param {Bool} useAbsCoord * @param {Bool} DisableIndirectCoord Whether disable indirect coord during moving object, preventing potential coord errors. */ Kekule.ChemObjOperation.MoveAndResizeObjs = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.MoveAndResizeObjs# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.MoveAndResizeObjs', /** @constructs */ initialize: function(/*$super, */objCoordAndSizeInfo, coordMode, useAbsCoord, editor) { this.tryApplySuper('initialize', [null, editor]) /* $super(null, editor) */; if (objCoordAndSizeInfo) this.setObjCoordAndSizeInfo(objCoordAndSizeInfo); this.setCoordMode(coordMode || Kekule.CoordMode.COORD2D); this.setUseAbsCoord(!!useAbsCoord); this.setPropStoreFieldValue('childOperations', []); }, /** @private */ initProperties: function() { this.defineProp('objCoordAndSizeInfo', {'dataType': DataType.ARRAY, 'serializable': false}); this.defineProp('coordMode', {'dataType': DataType.INT}); this.defineProp('useAbsCoord', {'dataType': DataType.BOOL}); this.defineProp('disableIndirectCoord', {'dataType': DataType.BOOL}); this.defineProp('childOperations', {'dataType': DataType.ARRAY, /*'setter': null,*/ 'serializable': false}); // private this.defineProp('indirectCoordObjs', {'dataType': DataType.ARRAY, 'serializable': false}); // private }, /** @private */ _setEnableIndirectCoordOfObjs: function(objs, enabled) { if (!objs) return; for (var i = 0, l = objs.length; i < l; ++i) { //console.log('set indirect enabled', objs[i].id, enabled); objs[i].setEnableIndirectCoord(enabled); } }, /** @private */ _prepareIndirectCoordObjs: function(childOpers) { var disableIndirectCoord = this.getDisableIndirectCoord(); var indirectCoordObjs = []; // try disable indirect coord first if (disableIndirectCoord) { for (var i = 0, l = childOpers.length; i < l; ++i) { var oper = childOpers[i]; if (oper && oper instanceof Kekule.ChemObjOperation.MoveAndResize) { var target = oper.getTarget(); if (target.hasProperty('enableIndirectCoord') && target.getEnableIndirectCoord()) { target.setEnableIndirectCoord(false); indirectCoordObjs.push(target); } } } } this.setIndirectCoordObjs(indirectCoordObjs); }, /** @private */ _prepareExecute: function() { var childOpers = this.getChildOperations(); if (!childOpers.length) // child move operation is empty, preparing new ones. Otherwise use the exisiting ones. { childOpers.length = 0; // clear first var objInfos = this.getObjCoordAndSizeInfo(); for (var i = 0, l = objInfos.length; i < l; ++i) { var info = objInfos[i]; var obj = info.obj; // create child move and resize operation var oper = new Kekule.ChemObjOperation.MoveAndResize(obj, info.newDimension, info.newCoord, this.getCoordMode(), this.getUseAbsCoord(), this.getEditor()); oper.setAllowCoordBorrow(this.getAllowCoordBorrow()); if (info.oldDimension) oper.setOldDimension(info.oldDimension); if (info.oldCoord) oper.setOldCoord(info.oldCoord); childOpers.push(oper); /* if (disableIndirectCoord && obj.hasProperty('enableIndirectCoord')) { obj.setEnableIndirectCoord(false); indirectCoordObjs.push(obj); } */ } } this._prepareIndirectCoordObjs(childOpers); }, /** @private */ _prepareReverse: function() { if (this.getDisableIndirectCoord()) { if (!this.getIndirectCoordObjs()) this._prepareIndirectCoordObjs(this.getChildOperations()); this._setEnableIndirectCoordOfObjs(this.getIndirectCoordObjs(), false); } }, /** @private */ _doneMoveAndResize: function() { this._setEnableIndirectCoordOfObjs(this.getIndirectCoordObjs(), true); }, /** @private */ doExecute: function(/*$super*/) { this.tryApplySuper('doExecute') /* $super() */; try { this._prepareExecute(); var opers = this.getChildOperations(); for (var i = 0, l = opers.length; i < l; ++i) { opers[i].execute(); } } finally { this._doneMoveAndResize(); } }, /** @private */ doReverse: function(/*$super*/) { try { this._prepareReverse(); var opers = this.getChildOperations(); for (var i = opers.length - 1; i >= 0; --i) { opers[i].reverse(); } } finally { this._doneMoveAndResize(); } this.tryApplySuper('doReverse') /* $super() */; } }); /** * Operation of sticking a stickable node to another chem object. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.BaseStructureNode} node Stickable structure node. * @param {Kekule.ChemObject} stickTarget The target chem object to be sticked. * * @property {Kekule.ChemObject} stickTarget The target chem object to be sticked. * @property {Hash} oldCoord If old coord is not set, this property will be automatically calculated when execute the operation. * @property {Int} coordMode * @property {Bool} useAbsBaseCoord */ Kekule.ChemObjOperation.StickTo = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.StickTo# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.StickTo', /** @constructs */ initialize: function(/*$super, */node, stickTarget, editor) { this.tryApplySuper('initialize', [node, editor]) /* $super(node, editor) */; if (stickTarget) this.setStickTarget(stickTarget); }, /** @private */ initProperties: function() { this.defineProp('stickTarget', {'dataType': Kekule.ChemObject, 'serializable': false}); this.defineProp('oldStickTarget', {'dataType': Kekule.ChemObject, 'serializable': false}); }, /** @private */ _canExecute: function(node) { return node && node.getAllowCoordStickTo && node.getAllowCoordStickTo(this.getStickTarget()); }, /** @private */ doExecute: function(/*$super*/) { this.tryApplySuper('doExecute') /* $super() */; var node = this.getTarget(); if (this._canExecute(node)) { if (this.getOldStickTarget() === undefined) this.setOldStickTarget(node.getCoordStickTarget() || null); node.setCoordStickTarget(this.getStickTarget()); } }, /** @private */ doReverse: function(/*$super*/) { var node = this.getTarget(); node.setCoordStickTarget(this.getOldStickTarget()); this.tryApplySuper('doReverse') /* $super() */; } }); /** * A class method to check if a node can be sticked to another chem object. * @param {Kekule.BaseStructureNode} node * @param {Kekule.ChemObject} dest * @param {Bool} canStickToStructFragment * @returns {Bool} */ Kekule.ChemObjOperation.StickTo.canStick = function(node, dest, canStickToStructFragment, canStickToSiblings) { var result = node && node.getAllowCoordStickTo && node.getAllowCoordStickTo(dest); // basic request if (dest) // dest can be set to null { result = result && dest && dest.getAbsCoordOfMode; // request of dest result = result && (dest !== node); result = result && (dest.getAcceptCoordStickFrom && dest.getAcceptCoordStickFrom(node)); result = result && (!dest.getCoordStickTarget || dest.getCoordStickTarget() !== node); result = result && (canStickToStructFragment || !(dest instanceof Kekule.StructureFragment)); if (result && !canStickToSiblings) { var p1 = node.getParent(); var p2 = dest.getParent(); result = (p1 !== p2) || (!p1 || !p2); } } return result; }; /** * Operation of adding a chem object to parent. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Kekule.ChemObject} parentObj Object should be added to. * @param {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling. * * @property {Kekule.ChemObject} parentObj Object should be added to. * @property {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling. */ Kekule.ChemObjOperation.Add = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.Add# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.Add', /** @constructs */ initialize: function(/*$super, */chemObj, parentObj, refSibling, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; this.setParentObj(parentObj); this.setRefSibling(refSibling); }, /** @private */ initProperties: function() { this.defineProp('parentObj', {'dataType': 'Kekule.ChemObject', 'serializable': false}); this.defineProp('refSibling', {'dataType': 'Kekule.ChemObject', 'serializable': false}); }, /** @private */ doExecute: function() { var parent = this.getParentObj(); var obj = this.getTarget(); if (parent && obj) { var sibling = this.getRefSibling() || null; this.notifyBeforeAddingByEditor(obj, parent, sibling); parent.insertBefore(obj, sibling); this.notifyAfterAddingByEditor(obj, parent, sibling); } }, /** @private */ doReverse: function() { var obj = this.getTarget(); /* var parent = obj.getParent? obj.getParent(): null; if (!parent) parent = this.getParentObj(); if (parent !== this.getParentObj()) console.log('[abnormal!!!!!!!]', parent.getId(), this.getParentObj().getId()); */ var parent = this.getParentObj(); if (parent && obj) { var sibling = this.getRefSibling(); if (!sibling) // auto calc { sibling = obj.getNextSibling(); this.setRefSibling(sibling); } this.notifyBeforeRemovingByEditor(obj, parent); parent.removeChild(obj); this.notifyAfterRemovingByEditor(obj, parent); } } }); /** * Operation of removing a chem object from its parent. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Kekule.ChemObject} parentObj Object should be added to. * @param {Kekule.ChemObject} refSibling Sibling after target object before removing. * * @property {Kekule.ChemObject} parentObj Object should be added to. * @property {Kekule.ChemObject} refSibling Sibling after target object before removing. * This property is used in reversing the operation. If not set, it will be calculated automatically in execution. */ Kekule.ChemObjOperation.Remove = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemObjOperation.Remove# */ { /** @private */ CLASS_NAME: 'Kekule.ChemObjOperation.Remove', /** @constructs */ initialize: function(/*$super, */chemObj, parentObj, refSibling, editor) { this.tryApplySuper('initialize', [chemObj, editor]) /* $super(chemObj, editor) */; this.setParentObj(parentObj); this.setRefSibling(refSibling); }, /** @private */ initProperties: function() { this.defineProp('parentObj', {'dataType': 'Kekule.ChemObject', 'serializable': false}); this.defineProp('ownerObj', {'dataType': 'Kekule.ChemObject', 'serializable': false}); this.defineProp('refSibling', {'dataType': 'Kekule.ChemObject', 'serializable': false}); // private this.defineProp('removedRelations', {'dataType': DataType.ARRAY, 'serializable': false}); }, /** @private */ _isInEditorSelection: function(obj) { var editor = this.getEditor(); return ((editor && editor.getSelection && editor.getSelection()) || []).indexOf(obj) >= 0; }, /** @private */ _getRelatedObjRefRelations: function(obj) { var result = []; var owner = obj.getOwner(); if (owner) { var rels = owner.findObjRefRelations({'dest': obj}, {'checkDestChildren': true}); if (rels && rels.length) { for (var i = 0, l = rels.length; i < l; ++i) { var stored = Object.extend({}, rels[i]); if (AU.isArray(stored.dest)) // clone the array stored.dest = AU.clone(stored.dest); result.push(stored); } } } return result; }, /** @private */ _restoreRelations: function(relations) { if (relations) { //console.log('restore relations', relations); for (var i = 0, l = relations.length; i < l; ++i) { var rel = relations[i]; rel.srcObj.setPropValue(rel.srcProp.name, rel.dest); } } }, /** @private */ doExecute: function() { var obj = this.getTarget(); var parent = this.getParentObj(); var owner = this.getOwnerObj(); if (!parent && obj.getParent) { parent = obj.getParent(); this.setParentObj(parent); } if (!owner && obj.getOwner) { owner = obj.getOwner(); this.setOwnerObj(owner); } if (parent && obj) { if (!this.getRefSibling()) { var sibling = obj.getNextSibling? obj.getNextSibling(): null; this.setRefSibling(sibling); } var removedRelations = this._getRelatedObjRefRelations(obj); this.setRemovedRelations(removedRelations); //console.log('store relations', removedRelations); //console.log('remove', obj.getId()); // ensure obj is also removed from editor's selection var editor = this.getEditor(); var needModifySelection = this._isInEditorSelection(obj); if (needModifySelection) editor.beginUpdateSelection(); //console.log('remove child', parent.getClassName(), obj.getClassName()); this.notifyBeforeRemovingByEditor(obj, parent); parent.removeChild(obj); this.notifyAfterRemovingByEditor(obj, parent); if (needModifySelection) { //console.log('remove from selection', obj.getId()); editor.removeFromSelection(obj); editor.endUpdateSelection(); } } }, /** @private */ doReverse: function() { var parent = this.getParentObj(); var owner = this.getOwnerObj(); var obj = this.getTarget(); if (parent && obj) { var sibling = this.getRefSibling(); if (owner) obj.setOwner(owner); this.notifyBeforeAddingByEditor(obj, parent, sibling); parent.insertBefore(obj, sibling); this._restoreRelations(this.getRemovedRelations()); this.setRemovedRelations([]); this.notifyAfterAddingByEditor(obj, parent, sibling); } } }); /** * A namespace for operation about Chem Structure instance. * @namespace */ Kekule.ChemStructOperation = {}; /** * Operation of adding a chem node to a structure fragment / molecule. * @class * @augments Kekule.ChemObjOperation.Add */ Kekule.ChemStructOperation.AddNode = Class.create(Kekule.ChemObjOperation.Add, /** @lends Kekule.ChemStructOperation.AddNode# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.AddNode' }); /** * Operation of removing a chem node from a structure fragment / molecule. * @class * @augments Kekule.ChemObjOperation.Remove * * @property {Array} linkedConnectors */ Kekule.ChemStructOperation.RemoveNode = Class.create(Kekule.ChemObjOperation.Remove, /** @lends Kekule.ChemStructOperation.RemoveNode# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.RemoveNode', /** @private */ initProperties: function() { //this.defineProp('linkedConnectors', {'dataType': DataType.ARRAY, 'serializable': false}); this.defineProp('linkedConnectorInfos', {'dataType': DataType.ARRAY, 'serializable': false}); }, /** @private */ doExecute: function(/*$super*/) { /* if (!this.getLinkedConnectors()) { this.setLinkedConnectors(Kekule.ArrayUtils.clone(this.getTarget().getLinkedConnectors())); } */ if (!this.getLinkedConnectorInfos()) { var node = this.getTarget(); var linkedConnectors = node.getLinkedConnectors(); var info = []; for (var i = 0, l = linkedConnectors.length; i < l; ++i) { var connector = linkedConnectors[i]; // if node is a subgroup, it may not directly linked to connector, we should find the direct linked child node var actualLinkedNode = node.getActualConnectedObjToConnector(connector) || node; var nodeIndex = connector.indexOfConnectedObj(node); var refSibling; if (nodeIndex < 0) refSibling = null; else refSibling = connector.getConnectedObjAt(nodeIndex + 1) || null; info.push({'connector': connector, 'refSibling': refSibling}); if (actualLinkedNode !== node) info.actualLinkedNode = actualLinkedNode; //console.log('execute', connector.getId(), node.getId(), actualLinkedNode && actualLinkedNode.getId(), refSibling && refSibling.getId()); } this.setLinkedConnectorInfos(info); } this.tryApplySuper('doExecute') /* $super() */ }, /** @private */ doReverse: function(/*$super*/) { this.tryApplySuper('doReverse') /* $super() */; /* var linkedConnectors = this.getLinkedConnectors(); //console.log('reverse node', this.getTarget().getId()); if (linkedConnectors && linkedConnectors.length) { //this.getTarget().setLinkedConnectors(linkedConnectors); var target = this.getTarget(); //console.log('reverse append connector', linkedConnectors.length); for (var i = 0, l = linkedConnectors.length; i < l; ++i) { //linkedConnectors[i].appendConnectedObj(target); target.appendLinkedConnector(linkedConnectors[i]); } } */ var linkedConnectorInfos = this.getLinkedConnectorInfos(); if (linkedConnectorInfos) { var node = this.getTarget(); for (var i = 0, l = linkedConnectorInfos.length; i < l; ++i) { var connector = linkedConnectorInfos[i].connector; var refSibling = linkedConnectorInfos[i].refSibling; var actualLinkedNode = linkedConnectorInfos[i].actualLinkedNode || node; //node._doAppendLinkedConnector(connector); //connector.insertConnectedObjBefore(node, refSibling); connector.insertConnectedObjBefore(actualLinkedNode, refSibling); } } } }); /** * Operation of replace a chem node with another one. * @class * @augments Kekule.Operation */ Kekule.ChemStructOperation.ReplaceNode = Class.create(Kekule.Operation, /** @lends Kekule.ChemStructOperation.ReplaceNode# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.ReplaceNode', /** @constructs */ initialize: function(/*$super, */oldNode, newNode, parentObj, editor) { this.tryApplySuper('initialize') /* $super() */; this.setOldNode(oldNode); this.setNewNode(newNode); this.setParentObj(parentObj); this.setEditor(editor); }, /** @private */ initProperties: function() { this.defineProp('oldNode', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false}); this.defineProp('newNode', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false}); this.defineProp('parentObj', {'dataType': 'Kekule.ChemStructureFragment', 'serializable': false}); this.defineProp('editor', {'dataType': 'Kekule.Editor.BaseEditor', 'serializable': false}); }, /** @private */ _isInEditorSelection: function(node) { var editor = this.getEditor(); return ((editor && editor.getSelection && editor.getSelection()) || []).indexOf(node) >= 0; }, /** @private */ doExecute: function() { var oldNode = this.getOldNode(); var newNode = this.getNewNode(); if (oldNode && newNode) { var parent = this.getParentObj(); if (!parent) { parent = oldNode.getParent(); this.setParentObj(parent); } if (parent.replaceNode) { var editor = this.getEditor(); var needModifySelection = this._isInEditorSelection(oldNode); if (needModifySelection) editor.beginUpdateSelection(); parent.replaceNode(oldNode, newNode); if (needModifySelection) { editor.removeFromSelection(oldNode); editor.addObjToSelection(newNode); editor.endUpdateSelection(); } } } }, /** @private */ doReverse: function() { var oldNode = this.getOldNode(); var newNode = this.getNewNode(); if (oldNode && newNode) { var parent = this.getParentObj() || newNode.getParent(); if (parent.replaceNode) { //console.log('reverse!'); var editor = this.getEditor(); var needModifySelection = this._isInEditorSelection(newNode); if (needModifySelection) editor.beginUpdateSelection(); parent.replaceNode(newNode, oldNode); if (needModifySelection) { editor.removeFromSelection(newNode) editor.addObjToSelection(oldNode); editor.endUpdateSelection(); } } } } }); /** * Operation of adding a chem connector to a structure fragment / molecule. * @class * @augments Kekule.ChemObjOperation.Add * * @param {Kekule.ChemObject} chemObject Target chem object. * @param {Kekule.ChemObject} parentObj Object should be added to. * @param {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling. * @param {Array} connectedObjs Objects that connected by this connector. * * @property {Array} connectedObjs Objects that connected by this connector. */ Kekule.ChemStructOperation.AddConnector = Class.create(Kekule.ChemObjOperation.Add, /** @lends Kekule.ChemStructOperation.AddConnector# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.AddConnector', /** @constructs */ initialize: function(/*$super, */chemObj, parentObj, refSibling, connectedObjs, editor) { this.tryApplySuper('initialize', [chemObj, parentObj, refSibling, editor]) /* $super(chemObj, parentObj, refSibling, editor) */; //this.setParentObj(parentObj); //this.setRefSibling(refSibling); this.setConnectedObjs(connectedObjs); }, /** @private */ initProperties: function() { this.defineProp('connectedObjs', {'dataType': DataType.ARRAY, 'serializable': false}); }, /** @private */ doExecute: function(/*$super*/) { this.tryApplySuper('doExecute') /* $super() */; var connObjs = Kekule.ArrayUtils.clone(this.getConnectedObjs()); if (connObjs && connObjs.length) { this.getTarget().setConnectedObjs(connObjs); } }, /** @private */ doReverse: function(/*$super*/) { this.tryApplySuper('doReverse') /* $super() */; } }); /** * Operation of removing a chem connector from a structure fragment / molecule. * @class * @augments Kekule.ChemObjOperation.Remove * * @property {Array} connectedObjs Objects that connected by this connector. * This property is used in operation reversing. If not set, value will be automatically calculated in operation executing. */ Kekule.ChemStructOperation.RemoveConnector = Class.create(Kekule.ChemObjOperation.Remove, /** @lends Kekule.ChemStructOperation.RemoveConnector# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.RemoveConnector', /** @private */ initProperties: function() { this.defineProp('connectedObjs', {'dataType': DataType.ARRAY, 'serializable': false}); }, /** @private */ doExecute: function(/*$super*/) { if (!this.getConnectedObjs()) { this.setConnectedObjs(Kekule.ArrayUtils.clone(this.getTarget().getConnectedObjs())); } this.tryApplySuper('doExecute') /* $super() */ }, /** @private */ doReverse: function(/*$super*/) { this.tryApplySuper('doReverse') /* $super() */; var connObjs = this.getConnectedObjs(); if (connObjs && connObjs.length) { this.getTarget().setConnectedObjs(connObjs); } } }); /** * The base operation of merging two nodes as one, acts as the parent class of MergeNodes and MergeNodesPreview. * @class * @augments Kekule.ChemObjOperation.Base * * @param {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode. * @param {Kekule.ChemStructureNode} dest Destination node. * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule. * * @property {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode. * @property {Kekule.ChemStructureNode} dest Destination node. * @property {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule. * @property {Bool} mergeConnectorPropsFromTarget If connectors are merged in this operation, whether copy some properties of target connector to dest. */ Kekule.ChemStructOperation.MergeNodesBase = Class.create(Kekule.ChemObjOperation.Base, /** @lends Kekule.ChemStructOperation.MergeNodesBase# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.MergeNodesBase', /** @constructs */ initialize: function(/*$super, */target, dest, enableStructFragmentMerge, editor) { this.tryApplySuper('initialize', [target, editor]) /* $super(target, editor) */; this.setDest(dest); this.setEnableStructFragmentMerge(enableStructFragmentMerge || false); this._refSibling = null; this._nodeParent = null; }, /** @private */ initProperties: function() { this.defineProp('dest', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false}); this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL}); this.defineProp('mergeConnectorPropsFromTarget', {'dataType': DataType.BOOL}); }, /** * Returns nodes connected with both node1 and node2. * @param {Kekule.ChemStructureNode} node1 * @param {Kekule.ChemStructureNode} node2 * @returns {Array} */ getCommonSiblings: function(node1, node2) { var siblings1 = node1.getLinkedObjs(); var siblings2 = node2.getLinkedObjs(); return Kekule.ArrayUtils.intersect(siblings1, siblings2); }, /** @private */ doExecute: function() { // do nothing, descendants should override }, /** @private */ doReverse: function() { // do nothing, descendants should override } }); /** * Operation of merging two nodes as one. * @class * @augments Kekule.ChemStructOperation.MergeNodesBase * * @param {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode. * @param {Kekule.ChemStructureNode} dest Destination node. * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule. * * @property {Array} relinkedConnectors Connectors changing conntected objects during merge. * @property {Array} removedConnectors Connectors removed during merge. */ Kekule.ChemStructOperation.MergeNodes = Class.create(Kekule.ChemStructOperation.MergeNodesBase, /** @lends Kekule.ChemStructOperation.MergeNodes# */ { /** @private */ CLASS_NAME: 'Kekule.ChemStructOperation.MergeNodes', /** @constructs */ initialize: function(/*$super, */target, dest, enableStructFragmentMerge, editor) { this.tryApplySuper('initialize', [target, dest, enableStructFragmentMerge, editor]) /* $super(target, dest, enableStructFragmentMerge, editor) */; this._refSibling = null; this._nodeParent = null; this._structFragmentMergeOperation = null; this._removeConnectorOperations = []; this._removeNodeOperation = null; this._modifyConnecorOperations = []; }, /** @private */ initProperties: function() { this.defineProp('relinkedConnectors', {'dataType': DataType.ARRAY, 'serializable': false}); this.defineProp('removedConnectors', {'dataType': DataType.ARRAY, 'serializable': false}); //this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL}); }, /** @private */ getMergeConnPropsOperation: function(fromConnector, toConnector) { var result; if (fromConnector instanceof Kekule.Bond && toConnector instanceof Kekule.Bond) { // check bond type and order if (fromConnector.getBondType() === toConnector.getBondType()) { var bondType = fromConnector.getBondType(); if (bondType === Kekule.BondType.COVALENT) { var fromOrder = fromConnector.getBondOrder(); var toOrder = toConnector.getBondOrder(); if (fromOrder > toOrder) // copy bond order property { result = new Kekule.ChemObjOperation.Modify(toConnector, {'bondOrder': fromOrder}); } } } } return result; }, /** @ignore */ doExecute: function() { var mergeConnectorProps = this.getMergeConnectorPropsFromTarget(); var connModifyOpers = this._modifyConnecorOperations || []; var fromNode = this.getTarget(); var toNode = this.getDest(); var structFragment = fromNode.getParentFragment(); var destFragment = toNode.getParentFragment(); if (structFragment !== destFragment) // from different molecule { //console.log('need merge mol'); if (this.getEnableStructFragmentMerge()) { this._structFragmentMergeOperation = new Kekule.ChemStructOperation.MergeStructFragment(structFragment, destFragment, this.getEditor()); //this._structFragmentMergeOperation = new Kekule.ChemStructOperation.MergeStructFragment(destFragment, structFragment); this._structFragmentMergeOperation.execute(); structFragment = destFragment; } else return null; } this._nodeParent = structFragment; structFragment.beginUpdate(); try { var editor = this.getEditor(); var removedConnectors = this.getRemovedConnectors(); var commonSiblings; if (!removedConnectors) // auto calc { connModifyOpers = []; // need calculate later commonSiblings = this.getCommonSiblings(fromNode, toNode); var removedConnectors = []; if (commonSiblings.length) // has common sibling between from/toNode, bypass bond between fromNode and sibling { for (var i = 0, l = commonSiblings.length; i < l; ++i) { var sibling = commonSiblings[i]; var connector = fromNode.getConnectorTo(sibling); if (connector && (connector.getConnectedObjCount() == 2)) // connector in target struct need to be removed { removedConnectors.push(connector); } } } var directConnector = fromNode.getConnectorTo(toNode); if (directConnector) removedConnectors.push(directConnector); this.setRemovedConnectors(removedConnectors); } var connectors = this.getRelinkedConnectors(); if (!connectors) // auto calc { var connectors = Kekule.ArrayUtils.clone(fromNode.getLinkedConnectors()) || []; connectors = Kekule.ArrayUtils.exclude(connectors, removedConnectors); this.setRelinkedConnectors(connectors); } // save fromNode's information this._refSibling = fromNode.getNextSibling(); for (var i = 0, l = connectors.length; i < l; ++i) { var connector = connectors[i]; var index = connector.indexOfConnectedObj(fromNode); connector.removeConnectedObj(fromNode); connector.insertConnectedObjAt(toNode, index); // keep the index is important, wedge bond direction is related with node sequence } // some properties of removed connector may need to be copied to dest connector if (mergeConnectorProps && !connModifyOpers.length && removedConnectors && removedConnectors.length) { if (!commonSiblings) commonSiblings = this.getCommonSiblings(fromNode, toNode); for (var i = 0, l = commonSiblings.length; i < l; ++i) { var sibling = commonSiblings[i]; var targetConnector = fromNode.getConnectorTo(sibling); if (removedConnectors.indexOf(targetConnector) >= 0) { var destConnector = toNode.getConnectorTo(sibling); if (destConnector) { var copyConnPropsOper = this.getMergeConnPropsOperation(targetConnector, destConnector); if (copyConnPropsOper) { connModifyOpers.push(copyConnPropsOper); } } } } } this._removeConnectorOperations = []; for (var i = 0, l = removedConnectors.length; i < l; ++i) { var connector = removedConnectors[i]; var oper = new Kekule.ChemStructOperation.RemoveConnector(connector, null, null, editor); oper.execute(); this._removeConnectorOperations.push(oper); } //structFragment.removeNode(fromNode); this._removeNodeOperation = new Kekule.ChemStructOperation.RemoveNode(fromNode, null, null, editor); this._removeNodeOperation.execute(); if (connModifyOpers) { this._modifyConnecorOperations = connModifyOpers; for (var i = 0, l = connModifyOpers.length; i < l; ++i) { connModifyOpers[i].execute(); } } } finally { structFragment.endUpdate(); } }, /** @ignore */ doReverse: function() { var fromNode = this.getTarget(); var toNode = this.getDest(); //var structFragment = fromNode.getParent(); //var structFragment = toNode.getParent(); var structFragment = this._nodeParent; structFragment.beginUpdate(); try { var connModifyOpers = this._modifyConnecorOperations; if (connModifyOpers) { for (var i = 0, l = connModifyOpers.length; i < l; ++i) { connModifyOpers[i].reverse(); } //this._modifyConnecorOperations = []; } /* console.log(fromNode.getParent(), fromNode.getParent() === structFragment, toNode.getParent(), toNode.getParent() === structFragment); */ //structFragment.insertBefore(fromNode, this._refSibling); this._removeNodeOperation.reverse(); if (this._removeConnectorOperations.length) { for (var i = this._removeConnectorOperations.length - 1; i >= 0; --i) { var oper = this._removeConnectorOperations[i]; oper.reverse(); } } this._removeConnectorOperations = []; var connectors = this.getRelinkedConnectors(); //console.log('reverse node merge2', toNode, toNode.getParent()); for (var i = 0, l = connectors.length; i < l; ++i) { var connector = connectors[i]; var index = connector.indexOfConnectedObj(toNode); connector.removeConnectedObj(toNode); connector.insertConnectedObjAt(fromNode, index); } } finally { structFragment.endUpdate(); } //console.log('reverse node merge', toNode, toNode.getParent()); if (this._structFragmentMergeOperation) { this._structFragmentMergeOperation.reverse(); } } }); /** * A class method to check if two nodes can be merged * @param {Kekule.ChemStructureNode} target * @param {Kekule.ChemStructureNode} dest * @param {Bool} canMergeStructFragment * @returns {Bool} */ Kekule.ChemStructOperation.MergeNodes.canMerge = function(target, dest, canMergeStructFragment, canMergeNeighborNodes) { // never allow merge to another molecule point (e.g. formula molecule) or su