UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,460 lines (1,399 loc) 126 kB
/** * @fileoverview * Implemetation of base class involves in displaying chem object (such as viewer or editor). * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /xbrowsers/kekule.x.js * requires /render/kekule.render.configs.js * requires /render/kekule.render.utils.js * requires /widgets/operation/kekule.actions.js * requires /widgets/kekule.widget.base.js * requires /widgets/kekule.widget.helpers.js * requires /widgets/kekule.widget.styleResources.js * requires /widgets/commonCtrls/kekule.widget.buttons.js * requires /widgets/commonCtrls/kekule.widget.tabViews.js * requires /widgets/commonCtrls/kekule.widget.dialogs.js * requires /widgets/commonCtrls/kekule.widget.formControls.js * requires /widgets/chem/kekule.chemWidget.base.js * requires /widgets/chem/kekule.chemWidget.dialogs.js */ (function(){ "use strict"; var PS = Class.PropertyScope; var AU = Kekule.ArrayUtils; var DU = Kekule.DomUtils; var ZU = Kekule.ZoomUtils; var CW = Kekule.ChemWidget; //var CWT = Kekule.ChemWidgetTexts; var CNS = Kekule.Widget.HtmlClassNames; var CCNS = Kekule.ChemWidget.HtmlClassNames; /** @ignore */ Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, { DISPLAYER_DRAWCONTEXT_PARENT: 'K-Chem-Displayer-DrawContext-Parent', ACTION_MOL_DISPLAY_TYPE: 'K-Chem-MolDisplayType', ACTION_MOL_DISPLAY_SKELETAL: 'K-Chem-MolDisplaySkeletal', ACTION_MOL_DISPLAY_CONDENSED: 'K-Chem-MolDisplayCondensed', ACTION_MOL_DISPLAY_WIRE: 'K-Chem-MolDisplayWire', ACTION_MOL_DISPLAY_BALLSTICK: 'K-Chem-MolDisplayBallStick', ACTION_MOL_DISPLAY_STICKS: 'K-Chem-MolDisplaySticks', ACTION_MOL_DISPLAY_SPACEFILL: 'K-Chem-MolDisplaySpaceFill', ACTION_MOL_HIDE_HYDROGENS: 'K-Chem-MolHideHydrogens', ACTION_MOL_AUTO_COORD_GENERATION: 'K-Chem-MolAutoCoordGeneration', ACTION_ZOOMIN: 'K-Chem-ZoomIn', ACTION_ZOOMOUT: 'K-Chem-ZoomOut', ACTION_RESET: 'K-Chem-Reset', ACTION_COPY: 'K-Chem-Copy', ACTION_RESET_ZOOM: 'K-Chem-ResetZoom', ACTION_LOADFILE: 'K-Chem-LoadFile', ACTION_LOADDATA: 'K-Chem-LoadData', ACTION_SAVEFILE: 'K-Chem-SaveFile', ACTION_CLEAROBJS: 'K-Chem-ClearObjs', ACTION_CONFIG: 'K-Chem-Config', DIALOG_CHOOSE_FILE_FORAMT: 'K-Chem-Dialog-Choose-File-Format', DIALOG_CHOOSE_FILE_FORAMT_FORMATBOX: 'K-Chem-Dialog-Choose-File-Format-FormatBox', DIALOG_CHOOSE_FILE_FORAMT_PREVIEWER_REGION: 'K-Chem-Dialog-Choose-File-Format-PreviewerRegion', DIALOG_CHOOSE_FILE_FORAMT_PREVIEWER: 'K-Chem-Dialog-Choose-File-Format-Previewer' }); /** * Root config class of {@link Kekule.ChemWidget.ChemObjDisplayer}. * @class * @augments Kekule.AbstractConfigs * * @property {Kekule.ChemWidget.ChemObjDisplayerIOConfigs} ioConfigs */ Kekule.ChemWidget.ChemObjDisplayerConfigs = Class.create(Kekule.AbstractConfigs, /** @lends Kekule.ChemWidget.ChemObjDisplayerConfigs# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.ChemObjDisplayerConfigs', /** @private */ initProperties: function() { this.addConfigProp('ioConfigs', 'Kekule.ChemWidget.ChemObjDisplayerIOConfigs'); this.addConfigProp('environment2DConfigs', 'Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs'); this.addConfigProp('environment3DConfigs', 'Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs'); }, /** @private */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; this.setPropStoreFieldValue('ioConfigs', new Kekule.ChemWidget.ChemObjDisplayerIOConfigs()); this.setPropStoreFieldValue('environment2DConfigs', new Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs()); this.setPropStoreFieldValue('environment3DConfigs', new Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs()); } }); /** * Config class of I/O options for {@link Kekule.ChemWidget.ChemObjDisplayer}. * @class * @augments Kekule.AbstractConfigs * * @property {Bool} canonicalizeBeforeSave Whether canonicalize molecules in displayer before saving them. * @property {Bool} autoGenerateCoordsAfterLoad Whether generate coords when loading molecule without atom coords (e.g. SMILES). */ Kekule.ChemWidget.ChemObjDisplayerIOConfigs = Class.create(Kekule.AbstractConfigs, /** @lends Kekule.ChemWidget.ChemObjDisplayerIOConfigs# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.ChemObjDisplayerIOConfigs', /** @private */ initProperties: function() { this.addBoolConfigProp('canonicalizeBeforeSave', true); this.addBoolConfigProp('autoGenerateCoordsAfterLoad', true); } }); /** * Config class of 2D rendering environment options for {@link Kekule.ChemWidget.ChemObjDisplayer}. * @class * @augments Kekule.AbstractConfigs * * @property {Bool} antialias Whether use a more aggressive antialias strategy than the browser's default. * @property {Float} antialiasBlurRatio The blur level to make the 2D rendering more smooth. * Effects will only be take when property {@link Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs.antialias} is true. * @property {Float} overSamplingRatio The oversampling ratio to make the 2D rendering more smooth. * Effects will only be take when property {@link Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs.antialias} is true. */ Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs = Class.create(Kekule.AbstractConfigs, /** @lends Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.ChemObjDisplayerEnvironmentConfigs', /** @private */ initProperties: function() { this.addBoolConfigProp('antialias', false); this.addFloatConfigProp('antialiasBlurRatio', 0); this.addFloatConfigProp('overSamplingRatio', 1); } }); /** * An widget related to display chem objects (especially molecules). * Note, this class is simply a base class for viewer and editor. User should not use this one directly. * @class * @augments Kekule.ChemWidget.AbstractWidget * * @param {Variant} parentOrElementOrDocument * @param {Kekule.ChemObject} chemObj * @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. * @param {Kekule.ChemWidget.ChemObjDisplayerConfigs} displayerConfigs Configs of current displayer. * * @property {Kekule.ChemWidget.ChemObjDisplayerConfigs} displayerConfigs Configs of current displayer. * @property {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. Read only. * @property {Kekule.ChemObject} chemObj Object to be drawn. Set this property will repaint the client. * @property {Bool} chemObjLoaded Whether the chemObj is successful loaded and drawn in viewer. * @property {Bool} resetAfterLoad Whether reset display (remove rotate, zoom and so on) after set a new chem obj. * @property {Object} renderConfigs Configuration for rendering. * @property {Int} moleculeDisplayType Display type of molecule in displayer. Value from {@link Kekule.Render.Molecule2DDisplayType} or {@link Kekule.Render.Molecule3DDisplayType}. * @property {Hash} drawOptions A series of params to render chem object. * @property {Float} zoom Zoom ratio to draw chem object, equal to drawOptions.zoom. * @property {Float} initialZoom Initial zoom when just loading a chem object. Default is 1. * @property {Bool} autoSize Whether the widget change its size to fit the dimension of chem object. * @property {Int} padding Padding between chem object and edge of widget, in px. Only works when autoSize is true. * @property {Hash} baseCoordOffset Usually displayer draw object at center of widget, use this property to make * the drawing center moved from widget center. * Note: this property is useless when autoSize == true. * @property {Hash} transformParams A combination of (@link Kekule.ChemWidget.ChemObjDisplayer.drawOptions} and (@link Kekule.ChemWidget.ChemObjDisplayer.baseCoordOffset}. * @property {String} backgroundColor Get or set background color of displayer. Default is transparent. * @property {Bool} enableLoadNewFile Whether open a external file to displayer is allowed. * @property {Array} allowedInputFormatIds Formats that shown in input file dialog. Default is null, means accept all available formats. * @property {Array} allowedOutputFormatIds Formats that shown in output file dialog. Default is null, means accept all available formats. * @property {String} defaultInputFormatId * @property {String} defaultOutputFormatId * @property {Hash} standardizationOptions Possible options when do standardization on molecule before saving. */ /** * Invoked when the a chem object (or null) is loaded into the displayer. * event param of it has one fields: {obj: Object}. * @name Kekule.ChemWidget.ChemObjDisplayer#load * @event */ /** * Invoked when the displayer is repainted. * event param of it has one fields: {obj: Object}. * @name Kekule.ChemWidget.ChemObjDisplayer#repaint * @event */ Kekule.ChemWidget.ChemObjDisplayer = Class.create(Kekule.ChemWidget.AbstractWidget, /** @lends Kekule.ChemWidget.ChemObjDisplayer# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.ChemObjDisplayer', /** @construct */ initialize: function(/*$super, */parentOrElementOrDocument, chemObj, renderType, displayerConfigs) { this._paintFlag = 0; // used internally //this._errorReportElem = null; // use internally this._bgColorMap = {}; // use internally this._contextTransformOpsMap = new Kekule.MapEx(); this._reactChemObjDrawBind = this._reactChemObjDraw.bind(this); this.setPropStoreFieldValue('resetAfterLoad', true); this.setPropStoreFieldValue('renderType', renderType || Kekule.Render.RendererType.R2D); // must set this value first this.setPropStoreFieldValue('displayerConfigs', displayerConfigs || this.createDefaultConfigs()); this.tryApplySuper('initialize', [parentOrElementOrDocument]) /* $super(parentOrElementOrDocument) */; if (chemObj) { this.setChemObj(chemObj); } //this.setUseCornerDecoration(true); this.setEnableLoadNewFile(true); //this.setPropStoreFieldValue('displayerConfigs', displayerConfigs || this.createDefaultConfigs()); }, /** @private */ doFinalize: function(/*$super*/) { //this.setChemObj(null); /* var r = this.getPropStoreFieldValue('boundInfoRecorder'); if (r) r.finalize(); this.setPropStoreFieldValue('boundInfoRecorder', null); */ this.clearSubViews(); this.setPropStoreFieldValue('chemObj', null); this.getPainter().finalize(); var b = this.getPropStoreFieldValue('drawBridge'); var ctx = this.getPropStoreFieldValue('drawContext'); if (ctx && b) b.releaseContext(ctx); if (b && b.finalize) b.finalize(); this.setPropStoreFieldValue('drawBridge', null); this.setPropStoreFieldValue('drawContext', null); if (this._contextTransformOpsMap) this._contextTransformOpsMap.finalize(); this.tryApplySuper('doFinalize') /* $super() */; }, /** @private */ initProperties: function() { this.defineProp('displayerConfigs', {'dataType': 'Kekule.ChemWidget.ChemObjDisplayerConfigs', 'serializable': false, 'scope': PS.PUBLIC}); this.defineProp('enableLoadNewFile', {'dataType': DataType.BOOL}); this.defineProp('allowedInputFormatIds', {'dataType': DataType.ARRAY}); this.defineProp('allowedOutputFormatIds', {'dataType': DataType.ARRAY}); this.defineProp('defaultInputFormatId', {'dataType': DataType.STRING, 'serializable': false}); this.defineProp('defaultOutputFormatId', {'dataType': DataType.STRING, 'serializable': false}); this.defineProp('standardizationOptions', {'dataType': DataType.HASH}); this.defineProp('resetAfterLoad', {'dataType': DataType.BOOL}); this.defineProp('chemObj', {'dataType': 'Kekule.ChemObject', 'serializable': false, 'scope': PS.PUBLISHED, 'setter': function(value) { var oldObj = this.getPropStoreFieldValue('chemObj'); //if (value !== oldObj) // some times oldObj itself may change and may need to repaint { this.setPropStoreFieldValue('chemObj', value); this.chemObjChanged(value, oldObj); } } }); this.defineProp('chemObjData', {'dataType': DataType.STRING, 'serializable': false, 'getter': null, 'setter': function(value) { var jsonObj; if (typeof(value) === 'object') jsonObj = value; else jsonObj = JSON.parse(value); //console.log('json', jsonObj, value); if (jsonObj) { var chemObj; chemObj = (jsonObj.format)? Kekule.IO.loadFormatData(jsonObj.data, jsonObj.format): (jsonObj.mimeType)? Kekule.IO.loadMimeData(jsonObj.data, jsonObj.mimeType): null; if (chemObj) this._tryAutoGenerateChemObjCoordsAndLoad(chemObj); } } }); this.defineProp('chemObjLoaded', {'dataType': DataType.BOOL, 'serializable': false, 'scope': PS.PUBLIC, 'setter': null, 'getter': function() { return this.getChemObj() && this.getPropStoreFieldValue('chemObjLoaded'); } }); this.defineProp('renderType', {'dataType': DataType.INT, 'serializable': false, 'enumSource': Kekule.Render.RendererType, 'scope': PS.PUBLISHED, 'setter': function(value) { if (!this.getAllowRenderTypeChange()) { Kekule.error(Kekule.$L('ErrorMsg.RENDER_TYPE_CHANGE_NOT_ALLOWED')); return; } var oldValue = this.getRenderType(); if (value !== oldValue) { this.setPropStoreFieldValue('renderType', value); this.resetRenderType(oldValue, value); } } }); this.defineProp('renderConfigs', {'dataType': DataType.OBJECT, 'serializable': false, 'scope': PS.PUBLIC, 'getter': function() { var result = this.getPropStoreFieldValue('renderConfigs'); if (!result) result = (this.getRenderType() === Kekule.Render.RendererType.R3D)? Kekule.Render.Render3DConfigs.getInstance(): Kekule.Render.Render2DConfigs.getInstance(); return result; }, 'setter': function(value) { this.setPropStoreFieldValue('renderConfigs', value); var painter = this.getPropStoreFieldValue('painter'); if (painter) painter.setRenderConfigs(this.getRenderConfigs()); } }); this.defineProp('backgroundColor', {'dataType': DataType.STRING, 'scope': PS.PUBLISHED, 'setter': function(value) { this.setPropStoreFieldValue('backgroundColor', value); //this.setBackgroundColorOfType(value, this.getRenderType()); this._bgColorMap[this.getRenderType().toString()] = value; this.backgroundColorChanged(); } }); this.defineProp('inheritedRenderStyles', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'getter': function() { var result = this.getPropStoreFieldValue('inheritedRenderStyles'); if (!result) { result = []; this.setPropStoreFieldValue('inheritedRenderStyles', result); } return result; }, 'setter': function(value) { this.setPropStoreFieldValue('inheritedRenderStyles', value || []); this.requestRepaint(); } }); this.defineProp('inheritedRenderColor', {'dataType': DataType.BOOL, 'scope': PS.PUBLISHED, 'getter': function() { var inheritedStyles = this.getInheritedRenderStyles(); return !!(inheritedStyles && (inheritedStyles.indexOf('color') >= 0)); }, 'setter': function(value) { if ((!!value) !== this.getInheritedRenderColor()) { var styles = this.getInheritedRenderStyles(); if (value) styles.push('color'); else Kekule.ArrayUtils.remove(styles, 'color'); this.setInheritedRenderStyles(styles); } } }); this.defineProp('inheritedRenderBackgroundColor', {'dataType': DataType.BOOL, 'scope': PS.PUBLISHED, 'getter': function() { var inheritedStyles = this.getInheritedRenderStyles(); return !!(inheritedStyles && (inheritedStyles.indexOf('backgroundColor') >= 0)); }, 'setter': function(value) { if ((!!value) !== this.getInheritedRenderBackgroundColor()) { var styles = this.getInheritedRenderStyles(); if (value) styles.push('backgroundColor'); else Kekule.ArrayUtils.remove(styles, 'backgroundColor'); this.setInheritedRenderStyles(styles); } } }); this.defineProp('enableCustomCssProperties', {'dataType': DataType.BOOL, 'setter': function(value){ if (value != this.getEnableCustomCssProperties()) { this.setPropStoreFieldValue('enableCustomCssProperties', !!value); this.requestRepaint(); } } }); //this.defineProp('actualOverSamplingRatio', {'dataType': DataType.FLOAT, 'setter': null, 'scope': PS.PRIVATE}); // private this.defineProp('transformParams', {'dataType': DataType.HASH, 'serializable': false, 'scope': PS.PUBLIC, 'getter': function() { var result = Object.extend({}, this.getDrawOptions()); result.screenCoordOffset = this.getBaseCoordOffset(); return result; }, 'setter': function(value) { var param = value || {}; this.setDrawOptions(param); this.setBaseCoordOffset(param.baseCoordOffset); this.drawOptionChanged(); } }); this.defineProp('drawOptions', {'dataType': DataType.HASH, 'serializable': false, 'scope': PS.PUBLIC, 'getter': function() { var result = this.getPropStoreFieldValue('drawOptions'); if (!result) { result = {}; this.setPropStoreFieldValue('drawOptions', result); } return result; }, 'setter': function(value) { this.setPropStoreFieldValue('drawOptions', value); this.drawOptionChanged(); } }); this.defineProp('zoom', {'dataType': DataType.FLOAT, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.zoom; }, 'setter': function(value) { this.zoomTo(value); return this; } }); this.defineProp('initialZoom', {'dataType': DataType.FLOAT}); this.defineProp('autofit', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.autofit; }, 'setter': function(value) { var op = this.getDrawOptions() || {}; op.autofit = value; return this.setDrawOptions(op); } }); this.defineProp('autoShrink', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.autoShrink; }, 'setter': function(value) { var op = this.getDrawOptions() || {}; op.autoShrink = value; return this.setDrawOptions(op); } }); this.defineProp('moleculeDisplayType', {'dataType': DataType.INT, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.moleculeDisplayType; }, 'setter': function(value) { this.getDrawOptions().moleculeDisplayType = value; this.drawOptionChanged(); return this; } }); this.defineProp('hideHydrogens', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.hideHydrogens; }, 'setter': function(value) { this.getDrawOptions().hideHydrogens = !!value; this.drawOptionChanged(); return this; } }); this.defineProp('allowCoordBorrow', {'dataType': DataType.BOOL, 'serializable': false, 'scope': PS.PUBLIC, 'getter': function() { return Kekule.oneOf(this.getDrawOptions().allowCoordBorrow, this.getRenderConfigs().getGeneralConfigs().getAllowCoordBorrow()); } }); //this.defineProp('zoom', {'dataType': DataType.FLOAT, 'serializable': false}); this.defineProp('autoSize', {'dataType': DataType.BOOL, 'serializable': false, 'setter': function(value) { if (value != this.getAutoSize()) { this.setPropStoreFieldValue('autoSize', value); if (value && this.allowAutoSize()) this.drawOptionChanged(); // force repaint } } }); this.defineProp('padding', {'dataType': DataType.INT, 'serializable': false, 'setter': function(value) { if (value != this.getPadding()) { this.setPropStoreFieldValue('padding', value); /* if (this.allowAutoSize()) this.drawOptionChanged(); // force repaint */ this.getDrawOptions().autofitContextPadding = value; this.drawOptionChanged(); } } }); this.defineProp('baseCoordOffset', {'dataType': DataType.Hash, 'serializable': false, 'setter': function(value) { this.setPropStoreFieldValue('baseCoordOffset', value); if (this.getChemObjLoaded()) this.repaint(); } }); // private properties this.defineProp('drawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function(slient) { var result = this.getPropStoreFieldValue('drawBridge'); if (!result && !this.__$drawBridgeInitialized$__) { this.__$drawBridgeInitialized$__ = true; // avoid call this.createDrawBridge() multiple times result = this.createDrawBridge(/*slient*/); this.setPropStoreFieldValue('drawBridge', result); } return result; } }); this.defineProp('drawContext', {'dataType': DataType.OBJECT, 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('drawContext'); if (!result) { var bridge = this.getDrawBridge(); if (bridge) { var elem = this.getDrawContextParentElem(); if (!elem) return null; else { //var dim = Kekule.HtmlElementUtils.getElemScrollDimension(elem); //var dim = Kekule.HtmlElementUtils.getElemClientDimension(elem); var dim = Kekule.HtmlElementUtils.getElemOffsetDimension(elem); result = bridge.createContext(elem, dim.width, dim.height, this.getContextCreationParams(this.getRenderType(), bridge)); /* if (result !== elem) Kekule.HtmlElementUtils.addClass(result, CNS.DYN_CREATED); */ // a little smaller than current element, avoid scroll bars in when setting CSS3's resize: both property this.setPropStoreFieldValue('drawContext', result); } } } return result; } }); this.defineProp('painter', {'dataType': 'Kekule.Render.ChemObjPainter', 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('painter'); if (!result) { result = this.createNewPainter(); } return result; } }); this.defineProp('rootRenderer', {'dataType': 'Kekule.Render.AbstractRenderer', 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function() { var p = this.getPainter(); return p? p.getRenderer(): null; } }); // private object to record all bound infos this.defineProp('boundInfoRecorder', {'dataType': 'Kekule.Render.BoundInfoRecorder', 'serializable': false, 'scope': Class.PropertyScope.PRIVATE, 'setter': null, 'getter': function() { var p = this.getRootRenderer(); return p && p.getBoundInfoRecorder(); } }); // private, sub views for individual objects in displayer this.defineProp('subViews', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null, 'getter': function(canCreate) { var result = this.getPropStoreFieldValue('subViews'); if (!result && canCreate) { result = []; this.setPropStoreFieldValue('subViews', result); } return result; } }); }, /** @ignore */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; this.setStandardizationOptions({'unmarshalSubFragments': false, 'clearHydrogens': false}); // do not auto clear explicit H }, /** @ignore */ doEndUpdate: function(modifiedPropNames) { this.tryApplySuper('doEndUpdate', [modifiedPropNames]); if (this._requestRepainting) // pending painting job in begin/endUpadte block { this.repaint(this._requestRepainting.overrideOptions); } }, /** @ignore */ elementBound: function(element) { this.setObserveElemResize(true); }, /** @ignore */ doWidgetShowStateChanged: function(/*$super, */isShown) { this.tryApplySuper('doWidgetShowStateChanged', [isShown]) /* $super(isShown) */; /* As position/size calculation may be wrong when displayed = false, during the show phase, whole context should be force repainted. */ if (isShown) this.setChemObj(this.getChemObj()); }, /** @ignore */ doFileDragDrop: function(/*$super, */files) { if (!files /* || files.length > 1 */) return this.tryApplySuper('doFileDragDrop') /* $super() */; else // if only one file is dropped in, output the file content { var self = this; (function() { Kekule.IO.loadFileData(files[0], function(chemObj, success) { if (success) { self.setChemObj(chemObj); } }); }).defer(); // call load later, avoid error in load process that prevent result true is returned return true; } }, /** @ignore */ notifyStyleAttribChanged: function(targetElem) { this.tryApplySuper('notifyStyleAttribChanged', [targetElem]); var inheritedStyles = this.getInheritedRenderStyles() || []; if (inheritedStyles.length) { this.requestRepaint(); } }, /** * Whether changing render type is allowed in current type of displayer. * Default is false, descendants may override this method. * @returns {Bool} */ getAllowRenderTypeChange: function() { return false; }, /** * Notify the render type has been changed. * Descendants may override this method. * @private */ resetRenderType: function(oldType, newType) { var chemObj = this.getChemObj(); var bridge = this.getPropStoreFieldValue('drawBridge'); var context = this.getPropStoreFieldValue('drawContext'); if (bridge) { if (context) bridge.releaseContext(context); } this.setPropStoreFieldValue('drawContext', null); this.setPropStoreFieldValue('drawBridge', null); this.__$drawBridgeInitialized$__ = false; // important, marks the draw bridge should be reinitialized var newBgColor = this.getBackgroundColorOfType(newType); this.getDrawOptions().moleculeDisplayType = this.getDefaultMoleculeDisplayType(newType); // reset display type //this.setBackgroundColor(newBgColor); this.setPropStoreFieldValue('backgroundColor', newBgColor); this.backgroundColorChanged(true); // notify back color change but not repaint, as painter currently is still old one //if (chemObj) // repaint //this.setChemObj(chemObj || null); // clear old draw options if oldObj is set this._doLoadOnObj(chemObj, {refreshOnly: true}); this.resetDisplay(); }, /** * Force to recreate drawing context and repaint. */ resetRenderEnvironment: function() { this.resetRenderType(null, this.getRenderType()); return this; }, /** * Returns environment configs of current render mode. * @private */ getDisplayerEnvironmentConfigs: function() { var envConfigs = (this.getCoordMode() === Kekule.CoordMode.COORD3D)? this.getDisplayerConfigs().getEnvironment3DConfigs(): this.getDisplayerConfigs().getEnvironment2DConfigs(); return envConfigs; }, /** @private */ getContextCreationParams: function(renderType, drawBridge) { var result; var renderConfig = this.getRenderConfigs(); var displayerConfig = this.getDisplayerConfigs(); if (renderType === Kekule.Render.RendererType.R3D) result = { 'alpha': true, 'antialias': renderConfig.getEnvironmentConfigs().getAntialias() }; else // 2D result = { 'alpha': true, 'antialias': displayerConfig.getEnvironment2DConfigs().getAntialias(), 'antialiasBlurRatio': displayerConfig.getEnvironment2DConfigs().getAntialiasBlurRatio(), 'overSamplingRatio': displayerConfig.getEnvironment2DConfigs().getOverSamplingRatio() }; return result; }, /** * Create a default editor config object. * Descendants may override this method. * @returns {Kekule.Editor.BaseEditorConfigs} * @private */ createDefaultConfigs: function() { return new Kekule.ChemWidget.ChemObjDisplayerConfigs(); }, /** * Returns coord mode according to current renderType. * @returns {Int} */ getCoordMode: function() { return (this.getRenderType() === Kekule.Render.RendererType.R3D)? Kekule.CoordMode.COORD3D: Kekule.CoordMode.COORD2D; }, /** * Set renderType according to coord mode. * @param {Int} coordMode */ setCoordMode: function(coordMode) { var rType = (coordMode === Kekule.CoordMode.COORD3D)? Kekule.Render.RendererType.R3D: Kekule.Render.RendererType.R2D; this.setRenderType(rType); }, /** @private */ loadPredefinedResDataToProp: function(propName, resData, success) { if (propName === 'chemObj') // only this property can be set by predefined resource { if (success) { try { //var ext = resData.uri? Kekule.UrlUtils.extractFileExt(resData.uri): null; var chemObj = Kekule.IO.loadTypedData(resData.data, resData.resType, resData.resUri); if (!chemObj) // try regard resType as format ID { chemObj = Kekule.IO.loadFormatData(resData.data, resData.resType); } if (chemObj) { this._tryAutoGenerateChemObjCoordsAndLoad(chemObj); } else Kekule.error(Kekule.$L('ErrorMsg.LOAD_CHEMDATA_FAILED')); //this.setChemObj(chemObj); } catch(e) { this.reportException(e); } } else // else, failed { // NOTE: just report a text msg rather than an exception object. // In IE and Chrome, it seems that error object can not cross context, so that may cause Illegal invocation error in Kekule.ExceptionHandler this.reportException(/*Kekule.ErrorMsg.CANNOT_LOAD_RES_OF_URI*/Kekule.$L('ErrorMsg.CANNOT_LOAD_RES_OF_URI') + resData.resUri || ''); //Kekule.throwException(Kekule.ErrorMsg.CANNOT_LOAD_RES_OF_URI + resData.resUri || ''); } } }, /** @private */ reportException: function(e, exceptionLevel) { /* var msg = Kekule.getExceptionMsg(e); if (!this._errorReportElem) // create error element { this._errorReportElem = this.getDocument().createElement('span'); this._errorReportElem.className = Kekule.Widget.HtmlClassNames.PART_ERROR_REPORT; this.getElement().appendChild(this._errorReportElem); } this._errorReportElem.innerHTML = msg; Kekule.StyleUtils.setDisplay(this._errorReportElem, ''); */ Kekule.raise(e, exceptionLevel || Kekule.ExceptionLevel.ERROR); }, /* @private */ /* hideExceptionReport: function() { if (this._errorReportElem) { Kekule.StyleUtils.setDisplay(this._errorReportElem, 'none'); } }, */ /** @private */ createDrawBridge: function(slient) { var M = (this.getRenderType() === Kekule.Render.RendererType.R3D)? Kekule.Render.DrawBridge3DMananger: Kekule.Render.DrawBridge2DMananger; var result = M.getPreferredBridgeInstance(); if (!result) // can not find suitable draw bridge { if (!slient) { var errorMsg = M.getUnavailableMessage() || Kekule.error(Kekule.$L('ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED')); if (errorMsg) this.reportException(errorMsg, Kekule.ExceptionLevel.NOT_FATAL_ERROR); } // Kekule.error(/*Kekule.ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED*/Kekule.$L('ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED')); } /* infinite loop, remove this part if (this.getBackgroundColor() && result.setClearColor) result.setClearColor(this.getDrawContext(), this.getBackgroundColor()); */ return result; }, /** * Returns the root element to hold the drawing context parents. * Descendants may override this method * @returns {HTMLElement} */ getClientElement: function() { return this.getElement(); }, /** * Returns parent element to create draw context inside. * Descendants can override this method. */ getDrawContextParentElem: function(disableAutoCreate) { //return this.getElement(); var result = this._drawContextParentElem; if (!result && !disableAutoCreate) // create new { result = this.getDocument().createElement('div'); // IMPORTANT: span may cause dimension calc problem of context this._drawContextParentElem = result; Kekule.HtmlElementUtils.addClass(result, CNS.DYN_CREATED); Kekule.HtmlElementUtils.addClass(result, CCNS.DISPLAYER_DRAWCONTEXT_PARENT); // IMPORTANT: force to fullfill the parent, otherwise draw context dimension calculation may have problem //result.style.display = 'block'; result.style.width = '100%'; result.style.height = '100%'; // insert as first child var root = this.getClientElement(); var currFirst = Kekule.DomUtils.getFirstChildElem(root); if (currFirst) root.insertBefore(result, currFirst); else root.appendChild(result); } return result; }, /** * Whether current painter can meet the requirement of auto size. * @returns {Bool} */ allowAutoSize: function() { // TODO: Currently autosize is not enabled in 3D mode return this.getRenderType() === Kekule.Render.RendererType.R2D; }, /** * Whether context and draw bridge can modify existing graphic content. * @returns {Bool} */ canModifyPartialGraphic: function(context) { var b = this.getDrawBridge(); return b.canModifyGraphic? b.canModifyGraphic(context || this.getDrawContext()): false; }, /** @private */ doInsertedToDom: function() { this.doResize(); }, /** @private */ doResize: function() { //console.log('refit draw context'); // when the size of chem viewer is changed, context should also be adjusted and object should be redrawn. this.refitDrawContext(this.isPainting()); // if is paiting, do not call repaint }, /** @private */ refitDrawContext: function(doNotRepaint) { //if (this.getDrawBridge(true) && this.getDrawContext()) if (this.getDrawBridge() && this.getDrawContext()) { //var dim = Kekule.HtmlElementUtils.getElemScrollDimension(this.getElement()); var dim = Kekule.HtmlElementUtils.getElemClientDimension(this.getDrawContextParentElem()); //console.log(this.getElement().id, dim, Kekule.HtmlElementUtils.getElemScrollDimension(this.getElement())); //this.getDrawBridge().setContextDimension(this.getDrawContext(), dim.width, dim.height); var success = this.changeContextDimension(dim); if (!doNotRepaint && success) this.repaint(); } }, /** * Returns dimension of context. * @returns {Hash} */ getContextDimension: function() { if (this.getDrawBridge() && this.getDrawContext()) return this.getDrawBridge().getContextDimension(this.getDrawContext()); else return null; }, /** @private */ _getContextOverSamplingRatio: function() { var envConfigs = this.getDisplayerEnvironmentConfigs(); var enableAntialias = envConfigs.getAntialias && envConfigs.getAntialias(); var overSamplingRatio = (enableAntialias && envConfigs.getOverSamplingRatio && envConfigs.getOverSamplingRatio()) || 1; return overSamplingRatio; }, /** * Change the dimension of context. * @param {Hash} newDimension * @returns {Bool} Return true if dimension change successfully. */ changeContextDimension: function(newDimension) { /* if (this.getDrawBridge() && this.getDrawContext()) { var width = newDimension.width; var height = newDimension.height; var elem = this.getDrawBridge().getContextElem(this.getDrawContext()); if (elem) // use transform CSS property to do resampling { var overSamplingRatio = this._getContextOverSamplingRatio(); if (overSamplingRatio !== 1 && Kekule.BrowserFeature.cssTranform) // oversampling enabled { var scale = 1 / overSamplingRatio; elem.style.transform = 'scale(' + scale + ',' + scale + ')'; elem.style.transformOrigin = '0 0'; //this.setPropStoreFieldValue('actualOverSamplingRatio', overSamplingRatio); // store the over sampling ratio for rendering this.getDrawBridge().setContextParam(this.getDrawContext(), 'overSamplingRatio', overSamplingRatio); } else { elem.style.transform = ''; //this.setPropStoreFieldValue('actualOverSamplingRatio', null); // store the over sampling ratio for rendering this.getDrawBridge().setContextParam(this.getDrawContext(), 'overSamplingRatio', null); } } //this._resizeContext(this.getDrawContext(), this.getDrawBridge(), width, height); this.doChangeContextDimension(width, height); //this.getDrawBridge().setContextDimension(this.getDrawContext(), newDimension.width, newDimension.height); return true; } else return false; */ return this.doChangeContextDimension(this.getDrawContext(), this.getDrawBridge(), newDimension, true); }, /** @private */ doChangeContextDimension: function(drawContext, drawBridge, newDimension, enableOverSampling) { //this._resizeContext(this.getDrawContext(), this.getDrawBridge(), width, height); if (drawBridge && drawContext) { var width = newDimension.width; var height = newDimension.height; var elem = drawBridge.getContextElem(drawContext); if (elem) // use transform CSS property to do resampling { var overSamplingRatio = this._getContextOverSamplingRatio(); if (overSamplingRatio !== 1 && Kekule.BrowserFeature.cssTranform && enableOverSampling) // oversampling enabled { /* width *= overSamplingRatio; height *= overSamplingRatio; */ var scale = 1 / overSamplingRatio; elem.style.transform = 'scale(' + scale + ',' + scale + ')'; elem.style.transformOrigin = '0 0'; //this.setPropStoreFieldValue('actualOverSamplingRatio', overSamplingRatio); // store the over sampling ratio for rendering drawBridge.setContextParam(drawContext, 'overSamplingRatio', overSamplingRatio); } else { elem.style.transform = ''; //this.setPropStoreFieldValue('actualOverSamplingRatio', null); // store the over sampling ratio for rendering drawBridge.setContextParam(drawContext, 'overSamplingRatio', null); } } this._resizeContext(drawContext, drawBridge, width, height); //this.getDrawBridge().setContextDimension(this.getDrawContext(), newDimension.width, newDimension.height); return true; } else return false; }, /** @private */ _resizeContext: function(context, bridge, width, height) { if (context && bridge) bridge.setContextDimension(context, width, height); }, /** @private */ clearContext: function() { var c = this.getPropStoreFieldValue('drawContext'); if (c) { var p = this.getPropStoreFieldValue('painter'); if (p) p.clear(c); this.getDrawBridge().clearContext(c); } }, /** @private */ createNewPainter: function(chemObj) { var old = this.getPropStoreFieldValue('painter'); if (old) { old.removeEventListener('draw', this._reactChemObjDrawBind); old.finalize(); } var drawBridge = this.getDrawBridge(); var result = drawBridge? (new Kekule.Render.ChemObjPainter(this.getRenderType(), chemObj || this.getChemObj(), this.getDrawBridge())): null; this.setPropStoreFieldValue('painter', result); if (result) result.addEventListener('draw', this._reactChemObjDrawBind); // create new bound info recorder //this.createNewBoundInfoRecorder(result); return result; }, /** * Returns whether the painter, draw bridge and context are ready, the rendering can be successfully performed. * @returns {Bool} */ isRenderable: function() { return !!(/*this.getPainter() &&*/ this.getDrawBridge() && this.getDrawContext()); //return !!(this.getPropStoreFieldValue('painter') && this.getDrawBridge() && this.getDrawContext()); }, /* @private */ /* createNewBoundInfoRecorder: function(renderer) { var old = this.getPropStoreFieldValue('boundInfoRecorder'); if (old) old.finalize(); var recorder = new Kekule.Render.BoundInfoRecorder(renderer); //recorder.setTargetContext(this.getObjContext()); this.setPropStoreFieldValue('boundInfoRecorder', recorder); }, */ /** * Called when chemObj property has been changed. * Usually this will cause the viewer repaint itself. * @param {Kekule.ChemObject} newObj * @private */ chemObjChanged: function(newObj, oldObj) { //if (newObj) //console.log('change to new Obj', newObj); this.doLoad(newObj); if (this.getResetAfterLoad() && oldObj) // clear old draw options if oldObj is set this.resetDisplay(); }, /** * Load and display chemObj in viewer * @param {Kekule.ChemObject} chemObj */ load: function(chemObj) { this.setChemObj(chemObj); }, /** * Do actual job of loading a chemObj. * @param {Kekule.ChemObject} chemObj * @ignore */ doLoad: function(chemObj, options) { this._doLoadOnObj(chemObj); }, /** @private */ _doLoadOnObj: function(chemObj, options) { var refreshOnly = options && options.refreshOnly; this.clearSubViews(); // clear all old subviews when loading a new chem object //console.log('doLoad', chemObj); this.refitDrawContext(true); // ensure the context size is correct, but not force repaint. //this.hideExceptionReport(); this.setPropStoreFieldValue('chemObjLoaded', false); //this.clearContext(); try { if (chemObj) { /* // debug chemObj.addOverrideRenderOptionItem({ 'atomColor': '#ff0000', 'bondColor': '#00ff00', //'useAtomSpecifiedColor': false, 'atomRadius': 3 }); */ var painter = this.createNewPainter(chemObj); if (painter) painter.setRenderConfigs(this.getRenderConfigs()); /* var drawOptions = this.getDrawOptions(); var context = this.getDrawContext(); var baseCoord; if (this.getAutoSize() && this.allowAutoSize()) // need to resize widget dimension { var padding = this.getPadding() || 0; var renderBox = painter.estimateScreenBox(context, baseCoord, drawOptions); var width = renderBox.x2 - renderBox.x1 + padding * 2; var height = renderBox.y2 - renderBox.y1 + padding * 2; this.getDrawBridge().setContextDimension(context, width, height); this.setWidth(width + 'px'); this.setHeight(height + 'px'); baseCoord = {'x': width / 2, 'y': height / 2}; } else { baseCoord = drawOptions? drawOptions.baseCoord: null; if (!baseCoord) { var dim = Kekule.HtmlElementUtils.getElemClientDimension(this.getElement()); baseCoord = {'x': dim.width / 2, 'y': dim.height / 2}; } } painter.draw(context, baseCoord, drawOptions); */ this.repaint(); this.setPropStoreFieldValue('chemObjLoaded', true); // indicate obj loaded successful if (!refreshOnly) { this.invokeEvent('load', {'obj': chemObj}); } } else // no object, clear { this.clearContext(); if (!refreshOnly) this.invokeEvent('load', {'obj': chemObj}); // even chemObj is null, this event should also be invoked } } catch(e) { this.clearContext(); this.reportException(e); //console.log(e); //throw e; } finally { this.doLoadEnd(this.getChemObj()); } }, /** * Called when a chem object is loaded into widget. * Descendants may override this method. * @param {Kekule.ChemObject} chemObj If loading process is failed, this param may be null. * @private */ doLoadEnd: function(chemObj) { // do nothing here }, /** * Load chem object from data of special MIME type or file format. * @param {Variant} data Usually text content. * @param {String} mimeType * @param {String} fromUrlOrFileName From which file or url is this data loaded. * @param {String} formatId */ loadFromData: function(data, mimeType, fromUrlOrFileName, formatId, objAfterLoadCallback) { try { if (!data) { this.setChemObj(null); return null; } else { //var ext = fromUrlOrFileName? Kekule.UrlUtils.extractFileExt(fromUrlOrFileName): null; var chemObj; if (formatId) chemObj = Kekule.IO.loadFormatData(data, formatId); else if (mimeType || fromUrlOrFileName) chemObj = Kekule.IO.loadTypedData(data, mimeType, fromUrlOrFileName); if (chemObj) { //this.setChemObj(chemObj); if (objAfterLoadCallback) this._tryAutoGenerateChemObjCoordsForLoading(chemObj, objAfterLoadCallback); else this._tryAutoGenerateChemObjCoordsAndLoad(chemObj); } else Kekule.error(/*Kekule.ErrorMsg.LOAD_CHEMDATA_FAILED*/Kekule.$L('ErrorMsg.LOAD_CHEMDATA_FAILED')); return chemObj; } } catch(e) { this.reportException(e); } }, /** * Load chem object from file object. * NOTE: browser must support File Reader API to use this method. * @param {File} file */ loadFromFile: function(file) { if (!file) this.setChemObj(null); else { var self = this; try { Kekule.IO.loadFileData(file, function(chemObj, success) { if (success) { //self.setChemObj(chemObj); self._tryAutoGenerateChemObjCoordsAndLoad(chemObj); } } ); } catch (e) { this.reportException(e); } } }, /** * Returns object in displayer that to be saved. * Usually this should be the chemObj itself. * Descendants may override this method. * @returns {Kekule.ChemObject} * @private */ getSavingTargetObj: function() { return this.getChemObj(); }, /** * Save loaded chem object to data. * @param {String} formatId * @param {Int} dataType Text or binary. Set null to use default type. * @param {Kekule.ChemObject} obj Object to save, default is current chemObj loaded in displayer. * @returns {Variant} Saved data. */ saveData: function(formatId, dataType, obj) { var obj = obj || this.getSavingTargetObj(); /* this.getChemObj()*/ this.prepareSaveData(obj); var writer = Kekule.IO.ChemDataWriterManager.getWriterByFormat(formatId, null, obj); if (writer) { var doCanonicalize = this._needToCanonicalizeBeforeSaving() && this.getDisplayerConfigs().getIoConfigs().getCanonicalizeBeforeSave(); if (doCanonicalize && obj.standardize) // canonicalize first { //var obj = obj.clone? obj.clone(true): obj; // clone with id var obj = this._cloneSavingTargetObj(obj); obj.standardize(this.getStandardizationOptions()); } if (!dataType) { var formatInfo = Kekule.IO.DataFormatsManager.getFormatInfo(formatId); dataType = formatInfo.dataType; } var data = writer.writeData(obj, dataType, formatId); return data; } else { Kekule.error(/*Kekule.ErrorMsg.NO_SUITABLE_WRITER_FOR_FORMAT*/Kekule.$L('ErrorMsg.NO_SUITABLE_WRITER_FOR_FORMAT')); return null; } }, /** @private */ _cloneSavingTargetObj: function(obj) { var result = obj.clone? obj.clone(true): obj; // clone with id return result; }, /** * Return whether this displayer need to canonicalize molecule before save. * Descendants may override this method. * @returns {Bool} * @private */ _needToCanonicalizeBeforeSaving: function() { return false; }, /** * Called before obj is saved. Descendants can overrride this method. */ prepareSaveData: function(obj) { // do nothing here }, /** @private */ _molNeedAutoCoordGeneration: function(mol, coordMode) { var result = (mol instanceof Kekule.StructureFragment) && (mol.getNodeCount() > 0) && !mol.nodesHasCoordOfMode(this.getCoordMode(), this.getAllowCoordBorrow(), true); return result; }, /** @private */ _isCoordGeneratorAvailable: function(coordMode) { if ( Kekule.Calculator && Kekule.Calculator.Services) { var serviceName = (coordMode === Kekule.CoordMode.COORD3D) ? Kekule.Calculator.Services.GEN3D : Kekule.Calculator.Services.GEN2D; return Kekule.Calculator.hasService(serviceName) } else return false; }, /** * Returns whether the coord auto generation can be done in current displayer. * @returns {Bool} */ canAutoGenerateCoordForChemObj: function(chemObj, coordMode) { if (!chemObj) chemObj = this.getChemObj(); if (Kekule.ObjUtils.isUnset(coordMode)) coordMode = this.getCoordMode(); var result = (chemObj && chemObj instanceof Kekule.StructureFragment && chemObj.hasCtab() && Kekule.Calculator && Kekule.Calculator.generateStructure && this._isCoordGeneratorAvailable(coordMode)); return result; }, /** @private */ _tryAutoGenerateChemObjCoords: function(chemObj, coordMode, callback) { if (Kekule.ObjUtils.isUnset(coordMode)) coordMode = this.getCoordMode(); /* if (chemObj instanceof Kekule.Molecule && this.getDisplayerConfigs().getIoConfigs().getAutoGenerateCoordsAfterLoad() && Kekule.Calculator && Kekule.Calculator.generateStructure) // can auto generate coords */ if (this.canAutoGenerateCoordForChemObj(chemObj, coordMode)) { var is3D = coordMode === Kekule.CoordMode.COORD3D; //var hasCoords = (chemObj.getNodeCount() <= 0) ||