UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,543 lines (1,467 loc) 280 kB
/** * @fileoverview * Editor for Kekule.ChemSpace. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /core/kekule.common.js * requires /render/kekule.render.base.js * requires /render/kekule.render.extensions.js * requires /render/kekule.render.kekule.render.utils.js * requires /widgets/operation/kekule.operations.js * requires /widgets/commonCtrls/kekule.widget.formControls.js * requires /widgets/commonCtrls/kekule.widget.dialogs.js * requires /widgets/chem/periodicTable/kekule.chemWidget.periodicTables.js * requires /widgets/chem/uiMarker/kekule.chemWidget.uiMarkers.js * requires /widgets/chem/editor/kekule.chemEditor.extensions.js * requires /widgets/chem/editor/kekule.chemEditor.editorUtils.js * requires /widgets/chem/editor/kekule.chemEditor.configs.js * requires /widgets/chem/editor/kekule.chemEditor.operations.js * requires /widgets/chem/editor/kekule.chemEditor.baseEditors.js * requires /widgets/chem/editor/kekule.chemEditor.repositories.js * requires /widgets/chem/editor/kekule.chemEditor.utilWidgets.js */ (function(){ "use strict"; var AU = Kekule.ArrayUtils; var CU = Kekule.CoordUtils; var CCNS = Kekule.ChemWidget.HtmlClassNames; //var CWT = Kekule.ChemWidgetTexts; /** @ignore */ Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, { CHEMSPACE_EDITOR: 'K-Chem-Space-Editor', CHEMSPACE_EDITOR2D: 'K-Chem-Space-Editor2D', CHEMSPACE_EDITOR3D: 'K-Chem-Space-Editor3D', CHEMEDITOR_ATOM_SETTER: 'K-ChemEditor-Atom-Setter', CHEMEDITOR_TEXT_SETTER: 'K-ChemEditor-Text-Setter', CHEMEDITOR_FORMULA_SETTER: 'K-ChemEditor-Formula-Setter' }); Kekule.globalOptions.add('chemWidget.editor', { 'enableSelect': true, 'enableFileDrop': true, 'allowCreateNewChild': true, 'autoCreateNewStructFragment': true, 'allowAppendDataToCurr': true, 'enableGesture': true, 'initOnNewDoc': true }); Kekule.globalOptions.add('chemWidget.editor.molManipulation', { /* 'enableMagneticMerge': true, 'enableNodeMerge': true, 'enableNeighborNodeMerge': true, 'enableConnectorMerge': true, 'enableStructFragmentMerge': true, */ 'enableNodeStick': true, 'enableStructFragmentStick': true, 'enableConstrainedMove': true, 'enableConstrainedRotate': true, 'enableConstrainedResize': true, 'enableDirectedMove': true }); Kekule.globalOptions.add('chemWidget.editor.bondManipulation', { 'enableBondModification': true, 'allowBondingToBond': false, 'autoSwitchBondOrder': false }); /** * A chem editor to edit chemspace object and other chem objects. * When load a chem object other than instance of Kekule.ChemSpace, an empty ChemSpace instance will * be created and loaded object should be insert into it. * @class * @augments Kekule.Editor.BaseEditor * @param {Variant} parentOrElementOrDocument * @param {Kekule.ChemObject} chemObj initially loaded chemObj. * @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. * @param {Kekule.Editor.BaseEditorConfigs} editorConfigs Configuration of this editor. * * @property {Kekule.ChemSpace} chemSpace ChemSpace loaded in this editor. * @property {Float} defBondLength * @property {Bool} allowCreateNewChild Whether new direct child of space can be created. * Note: if the space is empty, one new child will always be allowed to create. * @property {Bool} autoCreateNewStructFragment Whether new molecule object can be created in space. * Note: if property {@link Kekule.Editor.ChemSpaceEditor.allowCreateNewChild} is false, this property * will always be false. * @property {Bool} allowAppendDataToCurr Whether display "append data" check box in the dialog of data load action. */ Kekule.Editor.ChemSpaceEditor = Class.create(Kekule.Editor.BaseEditor, /** @lends Kekule.Editor.ChemSpaceEditor# */ { /** @private */ CLASS_NAME: 'Kekule.Editor.ChemSpaceEditor', /** @constructs */ initialize: function(/*$super, */parentOrElementOrDocument, chemObj, renderType, editorConfigs) { /* this.setPropStoreFieldValue('allowCreateNewChild', true); this.setPropStoreFieldValue('autoCreateNewStructFragment', true); this.setPropStoreFieldValue('allowAppendDataToCurr', true); */ var getOptionValue = Kekule.globalOptions.get; this.setPropStoreFieldValue('allowCreateNewChild', getOptionValue('chemWidget.editor.allowCreateNewChild', true)); this.setPropStoreFieldValue('autoCreateNewStructFragment', getOptionValue('chemWidget.editor.autoCreateNewStructFragment', true)); this.setPropStoreFieldValue('allowAppendDataToCurr', getOptionValue('chemWidget.editor.autoCreateNewStructFragment', true)); this.tryApplySuper('initialize', [parentOrElementOrDocument, chemObj, renderType, editorConfigs]) /* $super(parentOrElementOrDocument, chemObj, renderType, editorConfigs) */; this._containerChemSpace = null; // private field, used to mark that a extra chem space container is used }, /** @private */ initProperties: function() { this.defineProp('chemSpace', {'dataType': 'Kekule.ChemSpace', 'serializable': false, 'getter': function() { return this.getChemObj(); }, 'setter': function(value) { this.setChemObj(value); } }); this.defineProp('autoCreateNewStructFragment', {'dataType': DataType.BOOL, 'getter': function() { return this.getPropStoreFieldValue('autoCreateNewStructFragment') && this.canCreateNewChild(); } }); this.defineProp('allowCreateNewChild', {'dataType': DataType.BOOL}); this.defineProp('allowAppendDataToCurr', {'dataType': DataType.BOOL}); }, /** @ignore */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; //this.setFileDroppable(true); // defaultly turn on file drop function this.setFileDroppable(Kekule.globalOptions.get('chemWidget.editor.enableFileDrop', true)); }, /** * Returns whether new direct child can be created in current space. * This method will returns true of property {@link Kekule.Editor.ChemSpaceEditor.allowCreateNewChild} is true * or the space is empty. * @returns {Bool} */ canCreateNewChild: function() { return this.getChemSpace() && (this.getAllowCreateNewChild() || (this.getChemSpace().getChildCount() <= 0)); }, /** @ignore */ getActualDrawOptions: function(/*$super*/) { var result = this.tryApplySuper('getActualDrawOptions') /* $super() */; // a special field to ensure use explict size of space // rather than calc size based on child objects result.useExplicitSpaceSize = true; return result; }, /** @private */ doSetChemObj: function(/*$super, */value) { var old = this.getChemObj(); if (old !== value) { if (old && this._containerChemSpace) { old.finalize(); this._containerChemSpace = null; } if (value) { if (value instanceof Kekule.ChemSpace) { this._initChemSpaceDefProps(value); this.tryApplySuper('doSetChemObj', [value]) /* $super(value) */; } else { var space = this.createContainerChemSpace(null, value); //this._initChemSpaceDefProps(space, value); //space.appendChild(value); this._containerChemSpace = space; this.tryApplySuper('doSetChemObj', [space]) /* $super(space) */; } if (this.getEditorConfigs().getInteractionConfigs().getAutoExpandClientSizeAfterLoading()) { this.autoExpandChemSpaceSize(); } if (value instanceof Kekule.ChemSpace) { // auto scroll if (this.getEditorConfigs().getInteractionConfigs().getScrollToObjAfterLoading()) { var objs = value.getChildren(); if (objs && objs.length) this.scrollClientToObject(objs); } } } else this.tryApplySuper('doSetChemObj', [value]) /* $super(value) */; } else this.tryApplySuper('doSetChemObj', [value]) /* $super(value) */; }, /** @ignore */ getExportableClasses: function(/*$super*/) { var result = this.tryApplySuper('getExportableClasses') /* $super() */; // now result includes chemSpace // add child objects of chemspace to result var space = this.getChemSpace(); if (space) { /* for (var i = 0, l = space.getChildCount(); i < l; ++i) { var obj = space.getChildAt(i); if (obj && obj.getClass) Kekule.ArrayUtils.pushUnique(result, obj.getClass()); } */ var iteratorFunc = function(child){ if (child.isStandalone && child.isStandalone() && child.getClass) { var oClass = child.getClass(); Kekule.ArrayUtils.pushUnique(result, oClass); } }; space.iterateChildren(iteratorFunc, true); } return result; }, /** @ignore */ exportObjs: function(/*$super, */objClass) { var result = this.tryApplySuper('exportObjs', [objClass]) /* $super(objClass) */; if ((!result || !result.length) && objClass) // check child objects of chemSpace { result = []; var space = this.getChemSpace(); /* if (space) { for (var i = 0, l = space.getChildCount(); i < l; ++i) { var obj = space.getChildAt(i); if (obj && (obj instanceof objClass)) result.push(obj); } } */ var filter = function(child){ return child && (child instanceof objClass); }; result = space.filterChildren(filter, true); } return result; }, /** @ignore */ getSavingTargetObj: function(/*$super*/) { // if only one child in chemspace, save this obj alone (rather than the space). var space = this.getChemSpace(); var childCount = space.getChildCount(); if (childCount === 1) { return space.getChildAt(0); } else return this.tryApplySuper('getSavingTargetObj') /* $super() */; }, /** @ignore */ _cloneSavingTargetObj: function(obj) { var space = this.getChemSpace(); var childCount = space.getChildCount(); if (childCount === 1 && obj === space.getChildAt(0)) { // The objRef properties are related with chemspace, if clone obj only, the relation may be lost var clonedSpace = space.clone(true); return clonedSpace.getChildAt(0); } else return this.tryApplySuper('_cloneSavingTargetObj', [obj]); }, /** @ignore */ createDefaultConfigs: function() { //return new Kekule.Editor.ChemSpaceEditorConfigs(); return Kekule.Editor.ChemSpaceEditorConfigs.getInstance(); }, /** @private */ doCreateNewDocObj: function() { return this.createContainerChemSpace(); }, /** @private */ doLoad: function(/*$super, */chemObj) { // supply essential charge and radical markers this._supplyChemMarkersOnObj(chemObj); this.tryApplySuper('doLoad', [chemObj]) /* $super(chemObj) */; }, /** @private */ doLoadEnd: function(/*$super, */chemObj) { var result = this.tryApplySuper('doLoadEnd', [chemObj]) /* $super(chemObj) */; // calc def bond length var defBondLength = null; if (chemObj) { var connectors = chemObj.getAllContainingConnectors(); if (connectors && connectors.length) { var coordMode = (this.getRenderType() === Kekule.Render.RendererType.R3D)? Kekule.CoordMode.COORD3D: Kekule.CoordMode.COORD2D; defBondLength = Kekule.ChemStructureUtils.getConnectorLengthMedian(connectors, coordMode, this.getAllowCoordBorrow()); } } this.setDefBondLength(defBondLength); return result; }, /** @ignore */ resetDisplay: function(/*$super*/) { // called after loading a new chemObj, or creating a new doc this.tryApplySuper('resetDisplay') /* $super() */; this.resetClientDisplay(); }, /** * Reset the transform params of context and repaint the client. * @private */ resetClientDisplay: function(restoreScrollPosition) { // run rest later after updating object in editor, to ensure the context is really updated (even in begin/endUpdateObject block, e.g., in undo change space size operation) // IMPORTANT: if not called in a defer way, the resetClientDisplay may be run before endUpdateObject. In reset process, the // object is actually not updated in draw context (e.g., the box of text block is still the old one and returns a old container box), // then the _initialTransformParams are recalculated in resetClientDisplay but got a wrong result. // So, here we must ensure the resetClientDisplay run after endUpdateObject. if (this.isUpdatingObject()) // suspend the reset process after updating is done { var self = this; /* this.once('endUpdateObject', function(e){ if (e.target === self) self._resetClientDisplayCore(restoreScrollPosition); }); */ this._registerAfterUpdateObjectProc(function(){ self._resetClientDisplayCore(restoreScrollPosition); }); } else return this._resetClientDisplayCore(restoreScrollPosition); }, /** @private */ _resetClientDisplayCore: function(restoreScrollPosition) { // adjust editor size var space = this.getChemObj(); if (space) { var scrollPos; if (restoreScrollPosition) scrollPos = this.getClientScrollPosition(); //console.log('decide size', space.getScreenSize()); var screenSize = space.getScreenSize(); this.changeClientSize(screenSize.x, screenSize.y, this.getCurrZoom()); // scroll to top center var elem = this.getEditClientElem().parentNode; var visibleClientSize = Kekule.HtmlElementUtils.getElemClientDimension(elem); if (!restoreScrollPosition) this.scrollClientTo(0, (screenSize.x * this.getCurrZoom() - visibleClientSize.width) / 2); else this.scrollClientTo(scrollPos.y, scrollPos.x); } }, /** @ignore */ zoomChanged: function(/*$super, */zoomLevel) { this.tryApplySuper('zoomChanged') /* $super() */; var space = this.getChemObj(); if (space) { var screenSize = space.getScreenSize(); this.changeClientSize(screenSize.x, screenSize.y, zoomLevel); } }, /** @ignore */ createNewBoundInfoRecorder: function(/*$super, */renderer) { this.tryApplySuper('createNewBoundInfoRecorder', [renderer]) /* $super(renderer) */; var recorder = this.getBoundInfoRecorder(); if (recorder) // add event listener to update text box size { recorder.addEventListener('updateBasicDrawObject', this.reactUpdateBasicDrawObject, this); } }, /** @private */ reactUpdateBasicDrawObject: function(e) { var obj = e.obj; // TODO: use such a method to set text block size may not be a good approach if (obj && (obj instanceof Kekule.TextBlock)) { var boundInfo = e.boundInfo; this.updateTextBlockSize(obj, boundInfo); } }, /* @ignore */ objectChanged: function(/*$super, */obj, changedPropNames) { /* if (this.getCoordMode() === Kekule.CoordMode.COORD2D) // only works in 2D mode { if (obj instanceof Kekule.TextBlock) // size need to be recalculated { //console.log('text box changed', obj.getId()); // must not use setSize2D, otherwise a new object change event will be triggered //obj.setPropStoreFieldValue('size2D', {'x': null, 'y': null}); obj.__$needRecalcSize__ = true; // special flag, indicating to recalculate size } } */ var result = this.tryApplySuper('objectChanged', [obj, changedPropNames]) /* $super(obj, changedPropNames) */; //this.autoExpandChemSpaceSize(); return result; }, /** @ignore */ doManipulationEnd: function(/*$super*/) { if (this.getEditorConfigs().getInteractionConfigs().getAutoExpandClientSizeAfterManipulation()) { //this.autoExpandChemSpaceSize(); var autoExpandInfo = this._calcChemSpaceAutoExpandInfo(); if (autoExpandInfo) // need to expand { var opers = this._createChemSpaceAutoExpandOperations(autoExpandInfo); if (opers) { if (this.getEnableOperHistory()) { //console.log('append resize oper', opers); this._appendOpersToLastManipulationOperation(opers); } for (var i = 0, l = opers.length; i < l; ++i) { opers[i].execute(); // execute but not push to history } } } } return this.tryApplySuper('doManipulationEnd') /* $super() */; }, /** @private */ _appendOpersToLastManipulationOperation: function(opers) { var currManipulationOpers = this.getOperationsInCurrManipulation(); if (currManipulationOpers && currManipulationOpers.length) { var lastOper = currManipulationOpers[currManipulationOpers.length - 1]; var operHistory = this.getOperHistory(); if (operHistory) { var historyOperations = operHistory.getOperations(); // TODO: here we use the internal array structure of OperationHistory, may change in the future var historyIndex = historyOperations.indexOf(lastOper); if (historyIndex >= 0) // replace oper in history { var macroOperation; if (lastOper instanceof Kekule.MacroOperation) macroOperation = lastOper; else { macroOperation = new Kekule.MacroOperation([lastOper]); historyOperations.splice(historyIndex, 1, macroOperation); currManipulationOpers.splice(currManipulationOpers.length - 1, 1, macroOperation); } for (var i = 0, l = opers.length; i < l; ++i) { macroOperation.add(opers[i]); } } } } }, /** @private */ updateTextBlockSize: function(textBlock, boundInfo) { if (this.getCoordMode() !== Kekule.CoordMode.COORD2D) // only works in 2D mode return; /* var oldSize = textBlock.getSize2D(); if (Kekule.ObjUtils.notUnset(oldSize.x) || Kekule.ObjUtils.notUnset(oldSize.y)) // size already set, by pass return; */ //if (!textBlock.__$needRecalcSize__) if (!textBlock.getNeedRecalcSize()) return; var stype = boundInfo.shapeType; if (stype === Kekule.Render.BoundShapeType.RECT) { /* console.log('boundddddd', boundInfo); var coords = boundInfo.coords; // context coords var objCoord1 = this.contextCoordToObj(coords[0]); var objCoord2 = this.contextCoordToObj(coords[1]); var delta = Kekule.CoordUtils.substract(objCoord2, objCoord1); // must not use setSize2D, otherwise a new object change event will be triggered and a new update process will be launched textBlock.setPropStoreFieldValue('size2D', {'x': Math.abs(delta.x), 'y': Math.abs(delta.y)}); //textBlock.setSize2D({'x': Math.abs(delta.x), 'y': Math.abs(delta.y)}); //delete textBlock.__$needRecalcSize__; textBlock.setNeedRecalcSize(false); */ } }, /** @private */ createContainerChemSpace: function(id, containingChemObj) { //var result = new Kekule.ChemSpace(id); var result = new Kekule.ChemDocument(id); this._initChemSpaceDefProps(result, containingChemObj); if (containingChemObj) { result.appendChild(containingChemObj); // adjust child position var spaceSize = result.getSizeOfMode(this.getCoordMode(), this.getAllowCoordBorrow()); var coord, ratio; var objBox = Kekule.Render.ObjUtils.getContainerBox(containingChemObj, this.getCoordMode(), this.getAllowCoordBorrow()); if (this.getCoordMode() === Kekule.CoordMode.COORD2D) { /* var clientElem = this.getEditClientElem(); var clientDim = Kekule.HtmlElementUtils.getElemClientDimension(clientElem); var clientScrollX = clientDim.scrollTop; var clientScrollY = clientDim.scrollLeft; var padding = clientDim.height / 2; */ var padding = this.getEditorConfigs().getChemSpaceConfigs().getDefPadding(); var ratio = result.getObjScreenLengthRatio(); //console.log('adjust inside obj size', objBox, this.isShown()); } /* var oldObjCoord = containingChemObj.getCoordOfMode? containingChemObj.getCoordOfMode(this.getCoordMode(), this.getAllowCoordBorrow()) || {}: {}; */ var oldObjCoord = containingChemObj.getAbsBaseCoord? containingChemObj.getAbsBaseCoord(this.getCoordMode(), this.getAllowCoordBorrow()) || {}: {}; if (ratio && objBox) // 2D and calc padding { /* var oldObjCoord = containingChemObj.getCoordOfMode? containingChemObj.getCoordOfMode(this.getCoordMode()) || {}: {}; */ coord = Kekule.CoordUtils.divide(spaceSize, 2); //coord.y = spaceSize.y - /*(objBox.y2 - objBox.y1) / 2*/objBox.y2 - padding * ratio; //coord.y = spaceSize.y - Math.abs(objBox.y2 - objBox.y1) / 2 - padding * ratio; //coord.x -= (objBox.x2 + objBox.x1) / 2; /* coord.y = (oldObjCoord.y || 0) + spaceSize.y - padding * ratio - objBox.y2; coord.x += (oldObjCoord.x || 0) - (objBox.x2 + objBox.x1) / 2; */ var objBoxCenter = {x: (objBox.x1 + objBox.x2) / 2, y: (objBox.y1 + objBox.y2) / 2}; var newObjCenter = {x: coord.x, y: spaceSize.y - padding * ratio - (objBox.y2 - objBox.y1) / 2}; var centerDelta = Kekule.CoordUtils.substract(newObjCenter, objBoxCenter); coord = Kekule.CoordUtils.add(oldObjCoord, centerDelta); /* coord.y = spaceSize.y - padding * ratio - (objBox.y2 - objBox.y1) / 2 - objBoxCenter.y; coord.x = coord.x - (objBox.x2 + objBox.x1) / 2; */ //console.log(spaceSize, coord, objBox); } else { //var oldObjCoord = containingChemObj.getCoordOfMode(this.getCoordMode()) || {}; coord = Kekule.CoordUtils.divide(spaceSize, 2); /* coord.x -= (objBox.x2 + objBox.x1) / 2; coord.y -= (objBox.y2 + objBox.y1) / 2; if (this.getCoordMode() === Kekule.CoordMode.COORD3D) coord.z -= (objBox.z2 + objBox.z1) / 2; coord = Kekule.CoordUtils.add(coord, oldObjCoord); */ } /* if (containingChemObj.setCoordOfMode) containingChemObj.setCoordOfMode(coord, this.getCoordMode()); */ if (containingChemObj.setAbsBaseCoord) containingChemObj.setAbsBaseCoord(coord, this.getCoordMode()); //this.setObjCoord(containingChemObj, coord, Kekule.Render.CoordPos.CENTER); } return result; }, /** @private */ _initChemSpaceDefProps: function(chemSpace, containingChemObj, forceResetSize2D) { var configs = this.getEditorConfigs(); var chemSpaceConfigs = configs.getChemSpaceConfigs(); if (this.getCoordMode() === Kekule.CoordMode.COORD2D) // now only handles 2D size { var screenSize = chemSpace.getScreenSize(); if (!screenSize.x && !screenSize.y) { screenSize = chemSpaceConfigs.getDefScreenSize2D(); chemSpace.setScreenSize(screenSize); } if (!chemSpace.getDefAutoScaleRefLength()) { var refLength; if (containingChemObj && containingChemObj.getAllAutoScaleRefLengths) { var refLengths = containingChemObj.getAllAutoScaleRefLengths(this.getCoordMode(), this.getAllowCoordBorrow()); refLength = refLengths && refLengths.length? Kekule.ArrayUtils.getMedian(refLengths): null; } else { var refLengths = chemSpace.getAllAutoScaleRefLengths(this.getCoordMode(), this.getAllowCoordBorrow()); refLength = refLengths.length? Kekule.ArrayUtils.getMedian(refLengths): null; } if (!refLength) refLength = configs.getStructureConfigs().getDefBondLength(); chemSpace.setDefAutoScaleRefLength(refLength); } if (!chemSpace.getSize2D() || forceResetSize2D) { var refScreenLength = this.getRenderConfigs().getLengthConfigs().getDefBondLength(); var ratio = chemSpace.getDefAutoScaleRefLength() / refScreenLength; chemSpace.setObjScreenLengthRatio(ratio); chemSpace.setSize2D({'x': screenSize.x * ratio, 'y': screenSize.y * ratio}); } } if (chemSpace.setIsEditing) chemSpace.setIsEditing(true); }, /** @private */ _getChemSpaceObjScreenLengthRatio: function() { var chemSpace = this.getChemSpace(); var ratio = chemSpace.getObjScreenLengthRatio(); if (!ratio) { var refScreenLength = this.getRenderConfigs().getLengthConfigs().getDefBondLength(); ratio = chemSpace.getDefAutoScaleRefLength() / refScreenLength; } return ratio; }, /** * Change the size of current chemspace. * The screen size of the space will also be modified in 2D coord mode. * @param {Hash} size * @param {Int} coordMode */ changeChemSpaceSize: function(size, coordMode) { var chemSpace = this.getChemSpace(); chemSpace.beginUpdate(); try { chemSpace.setSizeOfMode(size, coordMode); // now only change screen size in 2D mode if (coordMode === Kekule.CoordMode.COORD2D) { /* var ratio = chemSpace.getObjScreenLengthRatio(); if (!ratio) { var refScreenLength = this.getRenderConfigs().getLengthConfigs().getDefBondLength(); ratio = chemSpace.getDefAutoScaleRefLength() / refScreenLength; } */ var ratio = this._getChemSpaceObjScreenLengthRatio(); if (ratio) { chemSpace.setScreenSize({'x': size.x / ratio, 'y': size.y / ratio}); } } } finally { chemSpace.endUpdate(); } this.resetClientDisplay(true); }, /** * Change the screen size of current chem space in editor. * The size2D of chemSpace will also be modified. * @param {Hash} screenSize */ changeChemSpaceScreenSize: function(screenSize) { var chemSpace = this.getChemSpace(); chemSpace.beginUpdate(); try { /* var ratio = chemSpace.getObjScreenLengthRatio(); if (!ratio) { var refScreenLength = this.getRenderConfigs().getLengthConfigs().getDefBondLength(); ratio = chemSpace.getDefAutoScaleRefLength() / refScreenLength; } */ var ratio = this._getChemSpaceObjScreenLengthRatio(); if (ratio) { // change size 2D chemSpace.setSize2D({'x': screenSize.x * ratio, 'y': screenSize.y * ratio}); } chemSpace.setScreenSize(screenSize); } finally { chemSpace.endUpdate(); } this.resetClientDisplay(true); }, /** * Shift coords of all direct children of chem space. * @private */ _shiftChemSpaceChildCoord: function(coordDelta, coordMode, scrollToNewPosition) { var chemSpace = this.getChemSpace(); var children = chemSpace.getChildren(); if (children) { var scrollCoord; if (scrollToNewPosition) { scrollCoord = this.getClientScrollCoord(Kekule.Editor.CoordSys.CHEM); //scrollCoord = CU.multiply(scrollCoord, -1); } this.beginUpdateObject(); try { chemSpace.beginUpdate(); try { var allowCoordBorrow = this.getAllowCoordBorrow(); for (var i = 0, l = children.length; i < l; ++i) { var child = children[i]; if (child && child.setCoordOfMode) { var oldCoord = child.getCoordOfMode(coordMode, allowCoordBorrow) || {}; child.setCoordOfMode(CU.add(oldCoord, coordDelta)); } /* if (coordDelta2D && child && child.setCoord2D) child.setCoord2D(CU.add(child.getCoord2D(), coordDelta2D)); if (coordDelta3D && child && child.setCoord3D) child.setCoord3D(CU.add(child.getCoord3D(), coordDelta3D)); */ } if (scrollToNewPosition) { //console.log('scrollToNewPosition', scrollCoord); this.scrollClientToCoord(scrollCoord, Kekule.Editor.CoordSys.CHEM); } } finally { chemSpace.endUpdate(); } } finally { this.endUpdateObject(); } } }, /** @private */ _calcExpandChemSpaceSizeToTargetObjsInfo: function(targetObjs, coordMode) { if (targetObjs && targetObjs.length) { var containerBoxInfo = this._getTargetObjsExposedContainerBoxInfo(targetObjs); var totalContainerBox = containerBoxInfo.totalBox; if (totalContainerBox) return this._calcExpandChemSpaceSizeToContainerBoxInfo(totalContainerBox, coordMode); } return null; }, /** @private */ _calcExpandChemSpaceSizeToContainerBoxInfo: function(containerBox, coordMode) { var CM = Kekule.CoordMode; var space = this.getChemSpace(); var is3D = (coordMode === CM.COORD3D); var coordFields = ['x', 'y']; if (is3D) coordFields.push('z'); var chemSpaceConfigs = this.getEditorConfigs().getChemSpaceConfigs(); var ObjScreenLengthRatio = this._getChemSpaceObjScreenLengthRatio(); var screenPadding = chemSpaceConfigs.getDefPadding(); var objSysPadding = screenPadding * ObjScreenLengthRatio; var currSize = is3D? space.getSize3D(): space.getSize2D(); var minMaxCoords = Kekule.BoxUtils.getMinMaxCoords(containerBox); // check minCoord, if coord less than zero + padding, need to expand var minCoord = minMaxCoords.min; var minShift = {}; var needMinShift = false; for (var i = 0, l = coordFields.length; i < l; ++i) { var coordValue = minCoord[coordFields[i]]; if (coordValue < objSysPadding) { minShift[coordFields[i]] = objSysPadding - coordValue; needMinShift = true; } else minShift[coordFields[i]] = 0; } // check maxCoord, if coord greater than size - padding, need to expand var maxCoord = minMaxCoords.max; var maxDelta = {}; var needMaxExpand = false; for (var i = 0, l = coordFields.length; i < l; ++i) { var coordValue = maxCoord[coordFields[i]]; if (coordValue > currSize[coordFields[i]] - objSysPadding) { maxDelta[coordFields[i]] = coordValue - (currSize[coordFields[i]] - objSysPadding); needMaxExpand = true; } else maxDelta[coordFields[i]] = 0; } // prepare to expand var expands = {}; var actualExpands = {}; if (needMinShift || needMaxExpand) { //console.log('do expand', needMinShift, minShift, needMaxExpand, maxDelta); var autoExpandScreenSize = is3D? chemSpaceConfigs.getAutoExpandScreenSize2D(): chemSpaceConfigs.getAutoExpandScreenSize3D(); var autoExpandSize = CU.multiply(autoExpandScreenSize, ObjScreenLengthRatio); for (var i = 0, l = coordFields.length; i < l; ++i) { var field = coordFields[i]; expands[field] = minShift[field] + maxDelta[field]; var scale = Math.ceil(expands[field] / autoExpandSize[field]); actualExpands[field] = scale * autoExpandSize[field]; } // do expand and shift var newSize = CU.add(currSize, actualExpands); var result = {'newSize': newSize}; if (needMinShift) result.coordShift = minShift; return result; } else return null; }, /** @private */ _expandChemSpaceSizeAndShiftChildrenCoords: function(newSize, coordShift, coordMode) { this.beginUpdateObject(); try { if (coordShift) this._shiftChemSpaceChildCoord(coordShift, coordMode, true); this.changeChemSpaceSize(newSize, coordMode); } finally { this.endUpdateObject(); } }, /** @private */ _expandChemSpaceSizeToContainerBox: function(containerBox, coordMode) { var detail = this._calcExpandChemSpaceSizeToContainerBoxInfo(containerBox, coordMode); if (detail) { this._expandChemSpaceSizeAndShiftChildrenCoords(detail.newSize, detail.coordShift, coordMode); } return detail; }, /** * Change the size of current chemspace, automatically expand it to display all targetObjs. * @param {Array} targetObjs * @param {Int} coordMode */ expandChemSpaceSizeToTargetObjs: function(targetObjs, coordMode) { if (Kekule.ObjUtils.isUnset(coordMode)) coordMode = this.getCoordMode(); if (targetObjs && targetObjs.length) { var containerBoxInfo = this._getTargetObjsExposedContainerBoxInfo(targetObjs); var totalContainerBox = containerBoxInfo.totalBox; if (totalContainerBox) this._expandChemSpaceSizeToContainerBox(totalContainerBox, coordMode); } return this; }, /** * Automatically expand the size of current chem space to display all child objects. */ autoExpandChemSpaceSize: function() { if (this._isAutoExpandingChemSpace) // a flag, avoid recursion calls return this; this._isAutoExpandingChemSpace = true; try { var space = this.getChemSpace(); var targetObjs = space.getChildren(); if (targetObjs) this.expandChemSpaceSizeToTargetObjs(targetObjs, this.getCoordMode()); } finally { this._isAutoExpandingChemSpace = false; } return this; }, /** * Returns the new size and coord shift to auto expand chem space. * @private */ _calcChemSpaceAutoExpandInfo: function() { var space = this.getChemSpace(); var targetObjs = space.getChildren(); if (targetObjs) return this._calcExpandChemSpaceSizeToTargetObjsInfo(targetObjs, this.getCoordMode()); else return null; }, /** @private */ _createChemSpaceAutoExpandOperations: function(expandInfo) { if (expandInfo) { var result = []; var space = this.getChemSpace(); var coordMode = this.getCoordMode(); /* if (expandInfo.coordShift) result.push(new Kekule.ChemSpaceEditorOperation.ShiftChildrenCoords(space, expandInfo.coordShift, coordMode, this)); if (expandInfo.newSize) result.push(new Kekule.ChemSpaceEditorOperation.ChangeSpaceSize(space, expandInfo.newSize, coordMode, this)); */ result.push(new Kekule.ChemSpaceEditorOperation.ChangeSpaceSizeAndShiftChildrenCoords(space, expandInfo.newSize, expandInfo.coordShift, coordMode, this)); return result; } else return null; }, /** * Create new molecule or structure fragment in chem space. * @param {String} id * @returns {Kekule.StructureFragment} */ createNewStructFragmentAnchor: function(id) { /* if (!id) // debug, auto add { id = 'M' + this.getChemObj().getChildCount(); } */ if (this.getAutoCreateNewStructFragment()) { var result = new Kekule.Molecule(id); this.getChemSpace().appendChild(result); return result; } else return null; }, /** * Create a new atom in a blank parentMol to growing bonds. * @param {Kekule.StructFragment} parentMol * @param {String} id * @param {Hash} absCoord * @param {String} isotopeId Set null to create a default one * @returns {Kekule.ChemStructureNode} */ createStructStartingAtom: function(parentMol, id, absCoord, isotopeId) { this.beginUpdateObject(); try { if (parentMol) { if (!isotopeId) { // create default one isotopeId = this.getEditorConfigs().getStructureConfigs().getDefIsotopeId(); } var initialNode = new Kekule.Atom(null, isotopeId); parentMol.appendNode(initialNode); initialNode.setAbsCoordOfMode(absCoord, this.getCoordMode()); return initialNode; } else return null; } finally { this.endUpdateObject(); } }, /** * Create new molecule or structure fragment in chem space, the fragment containing a * starting node (atom) to growing bond. * @param {String} id * @param {Hash} absCoord * @param {String} isotopeId Set null to create a default one * @returns {Kekule.ChemStructureNode} */ createNewStructFragmentAndStartingAtom: function(id, absCoord, isotopeId) { this.beginUpdateObject(); try { var struct; /* if (this.hasOnlyOneBlankStructFragment()) // only one blank molecule, use it as the new structure fragment's parent. struct = this.getChemSpace().getChildAt(0); else */ struct = this.createNewStructFragmentAnchor(id); if (struct) { return this.createStructStartingAtom(struct, id, absCoord, isotopeId); } else return null; } finally { this.endUpdateObject(); } }, /** * Check if there is only one blank structure fragment (with no node and connector or formula) in editor. * @returns {Bool} */ hasOnlyOneBlankStructFragment: function() { var result = false; if (this.getChemSpace().getChildCount() === 1) { var obj = this.getChemSpace().getChildAt(0); if (obj instanceof Kekule.StructureFragment) { if (obj.getNodeCount() <= 0 && obj.getConnectorCount() <= 0 && !obj.hasFormula()) // empty molecule result = true; } } return result; }, /** * If there is only one blank structure fragment (with no node and connector or formula) in editor, * returns it. * @returns {Kekule.StructureFragment} */ getOnlyOneBlankStructFragment: function() { if (this.hasOnlyOneBlankStructFragment()) return this.getChemSpace().getChildAt(0); }, /** * Returns whether a new standalone object (e.g. molecule) can be added to editor. * @returns {Bool} */ canAddNewStandaloneObject: function() { return this.getAllowCreateNewChild() || (this.getChemSpace().getChildCount() <= 0); }, /** * Returns whether a new structure fragment that doest not connected to any existing ones can be added to space. * This function returns true if property autoCreateNewStructFragment is true or there is an empty molecule in space. * @returns {Bool} */ canAddUnconnectedStructFragment: function() { return this.canAddNewStandaloneObject() || this.hasOnlyOneBlankStructFragment(); }, /** * Returns whether current editor allows clone objects. * @returns {Bool} */ canCloneObjects: function() { return this.getAllowCreateNewChild(); }, /** * Clone objects in space. * Note that this method only works when property allowCreateNewChild is true. * @param {Array} objects * @param {Hash} screenCoordOffset If this value is set, new cloned objects will be moved based on this coord. * @param {Bool} addToSpace If true, the cloned objects will add to current space immediately. * @param {Bool} keepRelations If true, the object reference relations between objects will be kept in the cloned ones. * @returns {Array} Actually cloned objects. */ cloneObjects: function(objects, screenCoordOffset, addToSpace, keepRelations) { var self = this; if (!this.getChemSpace()) return null; /* if (!this.getAllowCreateNewChild()) return null; */ var allowAddToSpace = this.getAllowCreateNewChild(); var isParentOfOneObj = function(obj, childObjs) { for (var i = 0, l = childObjs.length; i < l; ++i) { var childObj = childObjs[i]; if (/*(obj === childObj) ||*/ (childObj.isChildOf(obj))) return true; } return false; }; var removeUnessentialChildren = function(rootObj, refObj, reservedChildObjs) { if (reservedChildObjs.indexOf(refObj) >= 0) { AU.remove(reservedChildObjs, refObj); return; } if (rootObj.getChildAt && refObj.getChildAt) { var refChildObjCount = refObj.getChildCount(); for (var i = refChildObjCount - 1; i >= 0; --i) { var o = rootObj.getChildAt(i); if (!reservedChildObjs.length) { rootObj.removeChild(o); continue; } var refChildObj = refObj.getChildAt(i); if (reservedChildObjs.indexOf(refChildObj) >= 0) { AU.remove(reservedChildObjs, refChildObj); } else if (isParentOfOneObj(refChildObj, reservedChildObjs)) { if (refChildObj.getChildObjs) removeUnessentialChildren(o, refChildObj, reservedChildObjs); } else // can delete { rootObj.removeChild(o); } } } }; var _findParentObjOrSelf = function(targetObj, candidateObjs) { for (var i = 0, l = candidateObjs.length; i < l; ++i) { var candObj = candidateObjs[i]; if (targetObj === candObj || (targetObj.isChildOf && targetObj.isChildOf(candObj))) return candObj; } return null; }; var getRelatedObjRefRelations = function(standAloneObjs, owner) { var result = []; if (!owner) owner = self.getChemObj(); // the root chemspace var relations = owner.getObjRefRelations && owner.getObjRefRelations(); if (relations) { // check each relation for (var i = 0, l = relations.length; i < l; ++i) { var rel = relations[i]; var src = rel.srcObj; var dests = AU.toArray(rel.dest); var matchedSrc = _findParentObjOrSelf(src, standAloneObjs); var matchedDests = []; var matchAtLeastOneDest = false; if (matchedSrc) { for (var j = 0, k = dests.length; j < k; ++j) { var dest = dests[j]; var matchedDest = _findParentObjOrSelf(dest, standAloneObjs); if (matchedDest) matchAtLeastOneDest = true; matchedDests.push(matchedDest); } if (matchAtLeastOneDest) result.push({'relation': rel, 'srcParent': matchedSrc, 'destParents': matchedDests}); } } } return result; }; var getNewObjRefRelation = function(relationMatch, oldObjs, clonedObjs) { var oldRelation = relationMatch.relation; var oldSrcParent = relationMatch.srcParent; var oldDestParents = relationMatch.destParents; var indexStack, newSrcObj, newDestObjs = []; // src var srcIndex = oldObjs.indexOf(oldSrcParent); var newSrcParent = clonedObjs[srcIndex]; if (oldRelation.srcObj === oldSrcParent) newSrcObj = newSrcParent; else { indexStack = oldSrcParent.indexStackOfChild && oldSrcParent.indexStackOfChild(oldRelation.srcObj); if (indexStack) { newSrcObj = newSrcParent.getChildAtIndexStack && newSrcParent.getChildAtIndexStack(indexStack); } } if (!newSrcObj) return null; // dest var destIsArray = AU.isArray(oldRelation.dest); var oldDests = destIsArray? oldRelation.dest: [oldRelation.dest]; for (var i = 0, l = oldDestParents.length; i <l; ++i) { var oldDestParent = oldDestParents[i]; var destIndex = oldObjs.indexOf(oldDestParent); var newDestParent = clonedObjs[destIndex]; var newDestObj; if (oldDestParent) // parent has been cloned { if (oldDestParent === oldDests[i]) newDestObjs.push(newDestParent); else if (oldDests[i]) { indexStack = oldDestParent.indexStackOfChild && oldDestParent.indexStackOfChild(oldDests[i]); if (indexStack) { newDestObj = newDestParent.getChildAtIndexStack && newDestParent.getChildAtIndexStack(indexStack); if (newDestObj) newDestObjs.push(newDestObj); } } } } if (!newDestObjs.length) return null; var newDestValue = destIsArray? newDestObjs: newDestObjs[0]; var newRelation = {'srcObj': newSrcObj, 'srcProp': oldRelation.srcProp, 'dest': newDestValue}; return newRelation; }; var targetObjs = Kekule.ArrayUtils.toArray(objects); var standAloneObjs = []; var childObjMap = new Kekule.MapEx(); for (var i = 0, l = targetObjs.length; i < l; ++i) { var obj = targetObjs[i]; var standAloneObj = obj.getStandaloneAncestor? obj.getStandaloneAncestor(): obj; if (standAloneObj.clone) // object can be cloned { Kekule.ArrayUtils.pushUnique(standAloneObjs, standAloneObj); var mapItem = childObjMap.get(standAloneObj); if (!mapItem) { mapItem = []; childObjMap.set(standAloneObj, mapItem); } mapItem.push(obj); } } // find related relations var owner = this.getChemSpace(); var relationMatches = getRelatedObjRefRelations(standAloneObjs, owner); // start clone var space = this.getChemSpace(); var clonedObjs = []; var coordMode = this.getCoordMode(); var allowCoordBorrow = this.getAllowCoordBorrow(); var standAloneObjCount = standAloneObjs.length; /* for (var i = 0; i < standAloneObjCount; ++i) { var obj = standAloneObjs[i]; var clonedObj = obj.clone(); // clear ids to avoid conflict if (clonedObj.clearIds) clonedObj.clearIds(); // remove unessential child objects of cloned object removeUnessentialChildren(clonedObj, obj, childObjMap.get(obj)); if (addToSpace && allowAddToSpace) { space.appendChild(clonedObj); if (screenCoordOffset) { var coord = this.getObjectScreenCoord(clonedObj); var newCoord = Kekule.CoordUtils.add(coord, screenCoordOffset); this.setObjectScreenCoord(clonedObj, newCoord); } } clonedObjs.push(clonedObj); } */ //var interSpace = new Kekule.IntermediateChemSpace(); //try { // clone objects first, and put them in a inter chemspace to build relations for (var i = 0; i < standAloneObjCount; ++i) { var obj = standAloneObjs[i]; var clonedObj = obj.clone(); // clear ids to avoid conflict if (clonedObj.clearIds) clonedObj.clearIds(); clonedObjs.push(clonedObj); } //interSpace.appendChildren(clonedObjs); // copy cloned object to a inter chem space, to build relations // then map the new relations var newRelations = []; for (var i = 0, l = relationMatches.length; i < l; ++i) { var relationMatch = relationMatches[i]; var newRelation = getNewObjRefRelation(relationMatch, standAloneObjs, clonedObjs); if (newRelation) { newRelations.push(newRelation); /* newRelation.srcObj.setPropValue(newRelation.srcProp.name, newRelation.dest); // link objects console.log('new relation', newRelation, newRelation.srcObj.getOwner(), newRelation.srcObj, newRelation.srcObj.getPropValue(newRelation.srcProp.name)); */ } } // then remove unessential children for (var i = 0; i < standAloneObjCount; ++i) { var obj = standAloneObjs[i]; var clonedObj = clonedObjs[i]; // remove unessential child objects of cloned object removeUnessentialChildren(clonedObj, obj, childObjMap.get(obj)); if (addToSpace && allowAddToSpace) { space.appendChild(clonedObj); if (screenCoordOffset) { var coord = this.getObjectScreenCoord(clonedObj); var newCoord = Kekule.CoordUtils.add(coord, screenCoordOffset); this.setObjectScreenCoord(clonedObj, newCoord); } } } // at last rebuild the relations for (var i = 0, l = newRelations.length; i < l; ++i) { var newRelation = newRelations[i]; // check if new src/dest are not removed var finalDest; var hasNewSrcObj = _findParentObjOrSelf(newRelation.srcObj, clonedObjs); if (hasNewSrcObj) { if (AU.isArray(newRelation.dest)) { finalDest = []; for (var j = 0, k = newRelation.dest.length; j < k; ++j) { var destObj = newRelation.dest[j]; if (_findParentObjOrSelf(destObj, clonedObjs)) finalDest.push(destObj); } if (!finalDest.length) finalDest = null; } else { if (_findParentObjOrSelf(newRelation.dest, clonedObjs)) finalDest = newRelation.dest; } if (finalDest) // now we can map the new relation { newRelation.srcObj.setPropValue(newRelation.srcProp.name, finalDest); // link objects //console.log('new relation', newRelation, newRelation.srcObj.getOwner(), newRelation.srcObj, newRelation.srcObj.getPropValue(newRelation.srcProp.name)); } } } } //finally { //interSpace.finalize(); } childObjMap.finalize(); return clonedObjs; }, /** * Clone objects in editor's selection. * @param {Hash} coordOffset New cloned objects will be moved based on this coord. * If this value is not set, a default one will be used. * @param {Bool} addToSpace If true, the objects cloned will be added to space immediately. * @param {Bool} allowCloneSpace If true, the chemspace itself can be cloned when being selected. * Otherwise, the cloned targets are its children. * @returns {Array} Actually cloned objects. */ cloneSelection: function(coordOffset, addToSpace, allowCloneSpace) { var _getActualTargetObjs = function(objs, allowCloneSpace) { var result = []; for (var i = 0, l = objs.length; i < l; ++i) { var obj = objs[i]; if ((obj instanceof Kekule.ChemSpace) && !allowCloneSpace) // can not clone chemspace, use its children instead { var children = obj.getChildren(); AU.pushUnique(result, children); } else AU.pushUnique(result, obj); } return result; }; if (coordOffset === undefined) // use default one { coordOffset = this.getDefaultCloneScreenCoordOffset(); } var objs = this.getSelection(); objs = _getActualTargetObjs(objs, allowCloneSpace); var clonedObjs = this.cloneObjects(objs, coordOffset, addToSpace); if (addToSpace) this.setSelection(clonedObjs); return clonedObjs; }, /** * Returns default coord offset when doing clone selection job in editor. * @returns {Hash} */ getDefaultCloneScreenCoordOffset: function() { var screenOffset = this.getEditorConfigs().getInteractionConfigs().getClonedObjectScreenOffset() || 0; var coordMode = this.getCoordMode(); var coordOffset = {'x': screenOffset, 'y': screenOffset}; if (coordMode === Kekule.CoordMode.COORD3D) { coordOffset.z = screenOffset; } //coordOffset = this.translateCoord(coordOffset, Kekule.Editor.CoordSys.SCREEN, Kekule.Editor.CoordSys.OBJ); return coordOffset; }, /** * Supply essential charge and radical markers when loading a new chemObj. * @private */ _supplyChemMarkersOnObj: function(chemObj) { if (chemObj) { var structFragments = Kekule.ChemStructureUtils.getAllStructFragments(chemObj, true); if (structFragments || structFragments.length) { for (var i = 0, l = structFragments.length; i < l; ++i) { this._createLosingChemMarkerOnStructFragment(structFragments[i]); } } } }, /** @private */ _createLosingChemMarkerOnStructFragment: function(mol) { mol.beginUpdate(); try { // if (mol.getCharge && mol.getCharge()) if (mol.hasExplicitCharge && mol.hasExplicitCharge()) mol.fetchChargeMarker(true); // then the children var