UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,533 lines (1,400 loc) 176 kB
/** * @fileoverview * A default implementation of 2D molecule renderer. * @author Partridge Jiang */ /* * requires /core/kekule.common.js * requires /utils/kekule.utils.js * requires /core/kekule.structures.js * requires /core/kekule.reactions.js * requires /render/kekule.render.base.js * requires /render/kekule.render.utils.js * requires /render/kekule.baseTextRender.js * requires /chemdoc/kekule.commonChemMarkers.js * requires /localization/ */ (function() { var RT = Kekule.Render.BondRenderType; var D = Kekule.Render.TextDirection; var BU = Kekule.BoxUtils; var BO = Kekule.BondOrder; var CU = Kekule.CoordUtils; var oneOf = Kekule.oneOf; /** * Different renderer should provide different methods to draw element on context. * Those different implementations are wrapped in draw bridge classes. * Concrete bridge classes do not need to deprived from this class, but they * do need to implement all those essential methods. * * In all drawXXX methods, parameter options contains the style information to draw stroke or fill. * It may contain the following fields: * { * strokeWidth: Int, in pixel * strokeColor: String, '#rrggbb' * strokeDash: Bool, whether draw a dash line. * fillColor: String, '#rrggbb' * opacity: Float, 0-1 * } * * In all drawXXX methods, coord are based on context (not directly on screen). * * @class */ Kekule.Render.Abstract2DDrawBridge = Class.create( /** @lends Kekule.Render.Abstract2DDrawBridge# */ { /** @private */ CLASS_NAME: 'Kekule.Render.Abstract2DDrawBridge', /** @private */ CONTEXT_PARAMS_FIELD: '__$context_params__', /** * Create a context element for drawing. * @param {Element} parentElem * @param {Int} width Width of context, in px. * @param {Int} height Height of context, in px. * @param {Hash} params Additional params to create context. * //@param {Bool} doubleBuffered Whether use double buffer to make smooth drawing. * //@returns {Object} Context used for drawing. */ createContext: function(parentElem, width, height, params /* id, doubleBuffered */) { return null; }, /** * Destroy context created. * @param {Object} context */ releaseContext: function(context) { }, /** * Returns an additional param associated with context. * @param {Object} context * @param {String} key * @returns {Variant} */ getContextParam: function(context, key) { return (context[this.CONTEXT_PARAMS_FIELD] || {})[key]; }, /** * Set an additional param associated with context. * @param {Object} context * @param {String} key * @param {Variant} value */ setContextParam: function(context, key, value) { if (!context[this.CONTEXT_PARAMS_FIELD]) context[this.CONTEXT_PARAMS_FIELD] = {}; context[this.CONTEXT_PARAMS_FIELD][key] = value; }, /** @private */ _getOverSamplingRatio: function(context) { return this.getContextParam(context, 'overSamplingRatio') || null; }, /** * Get width and height of context. * @param {Object} context * @returns {Hash} {width, height} */ getContextDimension: function(context) { //return {}; var result = this._getContextRawDimension(context); var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) { result.width /= overSamplingRatio; result.height /= overSamplingRatio; } return result; }, /** * Get the raw width and height of context (regardless of oversampling). * @param {Object} context * @returns {Hash} {width, height} * @private */ _getContextRawDimension: function(context) { return {}; }, /** * Set new width and height of context. * @param {Object} context * @param {Int} width * @param {Int} height */ setContextDimension: function(context, width, height) { //return null; var dim = {'width': width, 'height': height}; var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) { dim.width *= overSamplingRatio; dim.height *= overSamplingRatio; } this._setContextRawDimension(context, dim.width, dim.height); }, /** * Set the raw width and height of context (regardless of oversampling). * @param {Object} context * @returns {Hash} {width, height} * @private */ _setContextRawDimension: function(context, width, height) { return null; }, /** * Get context related element. * @param {Object} context */ getContextElem: function(context) { }, /** * Set the view box of context. This method will also change context dimension to w/h if param changeDimension is not false. * @param {Object} context * @param {Int} x Top left x coord. * @param {Int} y Top left y coord. * @param {Int} w Width. * @param {Int} h Height. * @param {Bool} changeDimension */ setContextViewBox: function(context, x, y, w, h, changeDimension) { }, /** * Prepare the context for drawing. * @param {Object} context */ prepareContext: function(context) { }, /** * Clear the whole context. * @param {Object} context */ clearContext: function(context) { return null; }, /** * Set background color of content. * @param {Object} context * @param {String} color Color in '#RRGGBB' mode. Null means transparent. */ setClearColor: function(context, color) { }, /** * Set filter of the content. * @param {Object} context * @param {String} filter CSS filter string. */ setFilter: function(context, filter) { }, /** * Remove all filters from context. * @param {Object} context */ clearFilter: function(context) { }, /** * Transform a context based coord to screen based one (usually in pixel). * @param {Object} context * @param {Hash} coord * @return {Hash} */ transformContextCoordToScreen: function(context, coord) { //return coord; var result = coord; var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) result = Kekule.CoordUtils.divide(result, overSamplingRatio); return result; }, /** * Transform a screen based coord to context based one. * @param {Object} context * @param {Hash} coord * @return {Hash} */ transformScreenCoordToContext: function(context, coord) { //return coord; var result = coord; var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) result = Kekule.CoordUtils.multiply(result, overSamplingRatio); return result; }, transformContextLengthToScreen: function(context, length) { var result = length; var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) result /= overSamplingRatio; return result; }, transformScreenLengthToContext: function(context, length) { var result = length; var overSamplingRatio = this._getOverSamplingRatio(context); if (overSamplingRatio) result *= overSamplingRatio; return result; }, /** * Indicate whether this bridge and context can change glyph content or position after drawing it. * Raphael is a typical environment of this type while canvas should returns false. * @param {Object} context * @returns {Bool} */ canModifyGraphic: function(context) { return false; }, /** * Use SVG style path object to draw a path on context. * @param {Object} context * @param {Object} path * @param {Hash} options * @returns {Object} Element drawn on context */ drawPath: function(context, path, options) { }, /** * Draw a line on context. * @param {Object} context * @param {Hash} coord1 * @param {Hash} coord2 * @param {Hash} options * @returns {Object} Element drawn on context */ drawLine: function(context, coord1, coord2, options) { }, /** * Draw a triangle on context. * @param {Object} context * @param {Hash} coord1 * @param {Hash} coord2 * @param {Hash} coord3 * @param {Hash} options * @returns {Object} Element drawn on context */ drawTriangle: function(context, coord1, coord2, coord3, options) { }, /** * Draw a rectangle on context. * @param {Object} context * @param {Hash} coord1 * @param {Hash} coord2 * @param {Hash} options * @returns {Object} Element drawn on context */ drawRect: function(context, coord1, coord2, options) { }, /** * Draw a round corner rectangle on context. * @param {Object} context * @param {Hash} coord1 * @param {Hash} coord2 * @param {Number} cornerRadius * @param {Hash} options * @returns {Object} Element drawn on context */ drawRoundRect: function(context, coord1, coord2, cornerRadius, options) { }, /** * Draw a cirle on context. * @param {Object} context * @param {Hash} baseCoord * @param {Number} radius * @param {Hash} options * @returns {Object} Element drawn on context */ drawCircle: function(context, baseCoord, radius, options) { }, /** * Draw an arc on context * @param {Object} context * @param {Hash} centerCoord * @param {Number} radius * @param {Number} startAngle * @param {Number} endAngle * @param {Bool} anticlockwise * @param {Hash} options * @returns {Object} Element drawn on context */ drawArc: function(context, centerCoord, radius, startAngle, endAngle, anticlockwise, options) { }, /** * Draw an image on context * @param {Object} context * @param {String} src Src url of image. * @param {Hash} baseCoord * @param {Hash} size Target size ({x, y} of drawing image. * @param {Hash} options * @param {Function} callback Since image may need to be loaded from src on net, * this method may draw concrete image on context async. When the draw job * is done or failed, callback(success) will be called. * @returns {Object} Element drawn on context */ drawImage: function(context, src, baseCoord, size, options, callback) { }, /** * Draw the content of an image element on context * @param {Object} context * @param {HTMLElement} imgElem Source image element. * @param {Hash} baseCoord * @param {Hash} size Target size ({x, y} of drawing image. * @param {Hash} options * @returns {Object} Element drawn on context */ drawImageElem: function(context, imgElem, baseCoord, size, options) { }, /** * Create a nested structure on context. * @param {Object} context * @returns {Object} */ createGroup: function(context) { return null; }, /** * Ad an drawn element to group. * @param {Object} elem * @param {Object} group */ addToGroup: function(elem, group) { }, /** * Remove an element from group. * @param {Object} elem * @param {Object} group */ removeFromGroup: function(elem, group) { }, /** * Remove an element in context. * @param {Object} context * @param {Object} elem */ removeDrawnElem: function(context, elem) { }, /** * Export drawing content to a data URL for <img> tag to use. * @param {Object} context * @param {String} dataType Type of image data, e.g. 'image/png'. * @param {Hash} options Export options, usually this is a number between 0 and 1 * indicating image quality if the requested type is image/jpeg or image/webp. * @returns {String} */ exportToDataUri: function(context, dataType, options) { } }); /** * A base implementation of 2D chem object renderer. * You can call renderer.draw(context, chemObj, baseCoord, options) to draw the 2D structure, * where options can contain the settings of drawing style (strokeWidth, color...) and tranform params * (including scale, zoom, translate, rotateAngle...). * The options can also have autoScale, autofit and autoShrink (Bool) field, * if autoScale is true, the scale value will be determinate by renderer while when autofit is true, * the drawn element will try to fullfill the whole context area (without margin). The effect of autoShrink * equals to autofit in larger context area and equals to autoScale in smaller context area. * retainAspect will * decide whether aspect ratio will be preserved in autofit situation. * Note: zoom is not the same as scale. When scale is set or calculated, zoom will multiply on it and get the actual scale ratio. * for example, scale is 100 and zoom is 1.5, then the actual scale value will be 150. * * @augments Kekule.Render.AbstractRenderer * @param {Kekule.ChemObject} chemObj Object to be drawn. * @param {Object} drawBridge A object that implements the actual draw job. * @param {Hash} options Options to draw object. * @param {Object} renderConfigs Global configuration for rendering. * This property should be an instance of {@link Kekule.Render.Render2DConfigs}. * @param {Kekule.ObjectEx} parent Parent object of this renderer, usually another renderer or an instance of {@link Kekule.Render.ChemObjPainter}, or null. * * @property {Object} drawBridge A object that implements the actual draw job. Read only. * @property {Object} richTextDrawerClass Class of drawer to draw rich text on context. Default is {@link Kekule.Render.BaseRichTextDrawer}. * @class */ Kekule.Render.Base2DRenderer = Class.create(Kekule.Render.CompositeRenderer, // */ Kekule.Render.AbstractRenderer, /** @lends Kekule.Render.Base2DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.Base2DRenderer', /** @constructs */ initialize: function(/*$super, */chemObj, drawBridge, /*renderConfigs,*/ parent) { this.tryApplySuper('initialize', [chemObj, drawBridge, /*renderConfigs,*/ parent]) /* $super(chemObj, drawBridge, \*renderConfigs,*\ parent) */; /* if (!renderConfigs) this.setRenderConfigs(Kekule.Render.getRender2DConfigs()); // use default config */ //this.setRenderConfigs(null); this.__$redirectContextDebug__ = false; // special debug flag this._richTextDrawer = null; }, /** @private */ initProperties: function() { this.defineProp('richTextDrawerClass', {'dataType': DataType.CLASS, 'serializable': false}); }, /** @private */ getActualTargetContext: function(context) { /* if (this.getRedirectContext()) console.log('CONTEXT redirected', context.canvas.parentNode.className , this.getRedirectContext().canvas.parentNode.className); */ return this.getRedirectContext() || context; }, /** @private */ getRendererType: function() { return Kekule.Render.RendererType.R2D; }, /** * Indicate whether current render and context can measure text dimension before drawing it. * HTML Canvas is a typical environment of this type. * @param {Object} context * @returns {Bool} */ canMeasureText: function(context) { return this.getDrawBridge().canMeasureText(context); }, /** * Returns the standard draw length for calculation of autoScale. Usually this is the draw length of bond. * Descendants can override this method to use another length. * @returns {Float} */ getAutoScaleRefDrawLength: function(drawOptions) { return drawOptions.defBondLength; //this.getRenderConfigs().getLengthConfigs().getDefBondLength(); }, /** * Returns the reference length in object to calculate autoscale. * @returns {Float} */ getAutoScaleRefObjLength: function(chemObj, allowCoordBorrow) { var obj = chemObj || this.getChemObj(); if (obj.getAllAutoScaleRefLengths) { var lengths = obj.getAllAutoScaleRefLengths(Kekule.CoordMode.COORD2D, allowCoordBorrow); if (lengths && lengths.length) return Kekule.ArrayUtils.getMedian(lengths); } else //Kekule.error(Kekule.ErrorMsg.INAVAILABLE_AUTOSCALE_REF_LENGTH); return null; //1; }, /** * Returns an object to draw rich text on context. * @private */ getRichTextDrawer: function() { if (!this._richTextDrawer) { var C = this.getRichTextDrawerClass() || Kekule.Render.BaseRichTextDrawer; this._richTextDrawer = new C(this.getDrawBridge()); } return this._richTextDrawer; }, // bridged draw methods drawPath: function(context, path, options) { return this.getDrawBridge().drawPath(this.getActualTargetContext(context), path, options); }, drawLine: function(context, coord1, coord2, options) { if (this.__$redirectContextDebug__) { var op = Object.create(options); // debug if (this.getRedirectContext()) { op.color = op.strokeColor = '#ff0000'; } return this.getDrawBridge().drawLine(this.getActualTargetContext(context), coord1, coord2, op); } else return this.getDrawBridge().drawLine(this.getActualTargetContext(context), coord1, coord2, options); }, drawArrowLine: function(context, coord1, coord2, arrowParams, options) { var ctx = this.getActualTargetContext(context); if (!arrowParams) return this.drawLine(ctx, coord1, coord2, options); else { var result = this.createDrawGroup(ctx); // line var line = this.drawLine(ctx, coord1, coord2, options); this.addToDrawGroup(line, result); // arrow //if (arrowParams) { var dx = coord2.x - coord1.x; var dy = coord2.y - coord1.y; var alpha = Math.atan(dy / dx); var sign = Math.sign(dx) || 1; // with some default values var width = (arrowParams.width || 6) / 2; var length = arrowParams.length || 3; var beta = Math.atan(width / length); var l = Math.sqrt(Math.sqr(width) + Math.sqr(length)) * sign; var lcos1 = Math.cos(alpha - beta) * l; var lsin1 = Math.sin(alpha - beta) * l; var lcos2 = Math.cos(alpha + beta) * l; var lsin2 = Math.sin(alpha + beta) * l; line = this.drawLine(ctx, coord2, {'x': coord2.x - lcos1, 'y': coord2.y - lsin1}, options); this.addToDrawGroup(line, result); line = this.drawLine(ctx, coord2, {'x': coord2.x - lcos2, 'y': coord2.y - lsin2}, options); this.addToDrawGroup(line, result); } return result; } }, drawTriangle: function(context, coord1, coord2, coord3, options) { return this.getDrawBridge().drawTriangle(this.getActualTargetContext(context), coord1, coord2, coord3, options); }, drawRect: function(context, coord1, coord2, options) { var c1 = {'x': Math.min(coord1.x, coord2.x), 'y': Math.min(coord1.y, coord2.y)}; var c2 = {'x': Math.max(coord1.x, coord2.x), 'y': Math.max(coord1.y, coord2.y)}; /* // debug var op = Object.create(options); if (this.getRedirectContext()) { op.color = op.strokeColor = '#ff0000'; } return this.getDrawBridge().drawRect(this.getActualTargetContext(context), c1, c2, op); */ return this.getDrawBridge().drawRect(this.getActualTargetContext(context), c1, c2, options); }, drawRoundRect: function(context, coord1, coord2, cornerRadius, options) { var c1 = {'x': Math.min(coord1.x, coord2.x), 'y': Math.min(coord1.y, coord2.y)}; var c2 = {'x': Math.max(coord1.x, coord2.x), 'y': Math.max(coord1.y, coord2.y)}; return this.getDrawBridge().drawRoundRect(this.getActualTargetContext(context), c1, c2, cornerRadius, options); }, drawCircle: function(context, baseCoord, radius, options) { return this.getDrawBridge().drawCircle(this.getActualTargetContext(context), baseCoord, radius, options); }, drawArc: function(context, centerCoord, radius, startAngle, endAngle, anticlockwise, options) { return this.getDrawBridge().drawArc(this.getActualTargetContext(context), centerCoord, radius, startAngle, endAngle, anticlockwise, options); }, drawText: function(context, coord, text, options) { var drawer = this.getRichTextDrawer(); var rt = Kekule.Render.RichTextUtils.strToRichText(text, options); // debug if (this.__$redirectContextDebug__) { var op = Object.create(options || {}); if (this.getRedirectContext()) { op.color = op.strokeColor = '#ff0000'; } return drawer.drawEx(this.getActualTargetContext(context), coord, rt, op); } else return drawer.drawEx(this.getActualTargetContext(context), coord, rt, options/*op*/ /*, this.getRenderConfigs()*/); }, drawRichText: function(context, coord, richText, options) // note: return {drawnObj, boundRect} { var drawer = this.getRichTextDrawer(); // debug //console.log('draw richText', richText, options); if (this.__$redirectContextDebug__) { var op = Object.create(options || {}); if (this.getRedirectContext()) { op.color = op.strokeColor = '#ff0000'; } return drawer.drawEx(this.getActualTargetContext(context), coord, richText, op); } else return drawer.drawEx(this.getActualTargetContext(context), coord, richText, /*op*/ options /*, this.getRenderConfigs()*/); }, measureRichText: function(context, coord, richText, options) { var drawer = this.getRichTextDrawer(); return drawer.measure(context, coord, richText, options); }, drawImage: function(context, src, baseCoord, size, options, callback) { var self = this; // TODO: this approach need to be refined in the future return this.getDrawBridge().drawImage(this.getActualTargetContext(context), src, baseCoord, size, options, callback, function(){ return self.getActualTargetContext(context); }); }, drawImageElem: function(context, imgElem, baseCoord, size, options) { if (imgElem.complete) // image already been loaded, can draw now { //console.log('do actual draw'); return this.getDrawBridge().drawImageElem(this.getActualTargetContext(context), imgElem, baseCoord, size, options); } else // need to bind to load event, try draw later { var XEvent = Kekule.X.Event; if (XEvent) { var self = this; var unlinkImgDrawProc = function() { XEvent.removeListener(imgElem, 'load', updateImgDrawing); }; var updateImgDrawing = function() { //console.log('update draw', imgElem.complete); unlinkImgDrawProc(imgElem, updateImgDrawing); // force update the render, force redraw again self.update(self.getActualTargetContext(context), [{'obj': self.getChemObj()}], Kekule.Render.ObjectUpdateType.MODIFY); }; XEvent.addListener(imgElem, 'load', updateImgDrawing); XEvent.addListener(imgElem, 'error', unlinkImgDrawProc); } } }, createDrawGroup: function(context) { return this.getDrawBridge().createGroup(this.getActualTargetContext(context)); }, addToDrawGroup: function(elem, group) { return this.getDrawBridge().addToGroup(elem, group); }, removeFromDrawGroup: function(elem, group) { return this.getDrawBridge().removeFromGroup(elem, group); }, /** * Remove an element in context. * @param {Object} context * @param {Object} elem */ removeDrawnElem: function(context, elem) { return this.getDrawBridge().removeDrawnElem(this.getActualTargetContext(context), elem); }, /** * Transform a screen based coord to context based one. * Note that only 2D renderer can map screen coord back. * @param {Object} context * @param {Hash} coord * @return {Hash} */ transformScreenCoordToContext: function(context, coord) { return this.doTransformScreenCoordToContext(this.getActualTargetContext(context), coord); }, /** @private */ doTransformScreenCoordToContext: function(context, coord) { var b = this.getDrawBridge(); return (b && b.transformScreenCoordToContext)? b.transformScreenCoordToContext(this.getActualTargetContext(context), coord): coord; } }); /** * A base class to render a chem object. * @class * @augments Kekule.Render.Base2DRenderer */ Kekule.Render.ChemObj2DRenderer = Class.create(Kekule.Render.Base2DRenderer, /** @lends Kekule.Render.ChemObj2DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.ChemObj2DRenderer', /** @ignore */ _getRenderSortIndex: function(/*$super*/) { var obj = this.getChemObj(); if (obj && obj.coordStickTarget && obj.getCoordStickTarget()) return 1; return this.tryApplySuper('_getRenderSortIndex') /* $super() */; }, /** @private */ doEstimateSelfObjBox: function(context, options, allowCoordBorrow) { /* var box; var o = this.getChemObj(); if (o.getExposedContainerBox2D) box = o.getExposedContainerBox2D(allowCoordBorrow); else if (o.getContainerBox2D) box = o.getContainerBox2D(allowCoordBorrow); else box = null; return box; */ return Kekule.Render.ObjUtils.getContainerBox(this.getChemObj(), this.getCoordMode(), allowCoordBorrow); }, /** @private */ doEstimateRenderBox: function(context, baseCoord, options, allowCoordBorrow) { var objBox = this.estimateObjBox(context, options, allowCoordBorrow); if (objBox) // a general approach is to scale the chem object box to context's scope { var p = this.prepareTransformParams(context, baseCoord, options, objBox); //var transformParams = this.getFinalTransformParams(context, p); var transformParams = p; //.transformParams; return BU.transform2D(objBox, transformParams); } else return null; }, /** @private */ doGetAutoBaseCoord: function(drawOptions) { var transformParams = drawOptions.transformParams; if (transformParams) { var obj = this.getChemObj(); var objCoord = obj.getAbsBaseCoord? obj.getAbsBaseCoord(Kekule.CoordMode.COORD2D): obj.getAbsBaseCoord2D? obj.getAbsBaseCoord2D(): null; // console.log('autoCoord', objCoord, transformParams); if (objCoord) { return Kekule.CoordUtils.transform2D(objCoord, transformParams); } } return null; }, /** @ignore */ doDraw: function(/*$super, */context, baseCoord, options) { var medianObjRefLength = this.getAutoScaleRefObjLength(this.getChemObj(), options.allowCoordBorrow); options.medianObjRefLength = medianObjRefLength || options.defScaleRefLength; // since options passed by draw method is already protected, we are not worry about change it here. this.prepareTransformParams(context, baseCoord, options); this.prepareGeneralOptions(context, options); return this.tryApplySuper('doDraw', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; }, prepareGeneralOptions: function(context, options) { /* var configs = this.getRenderConfigs(); if (configs) { // since options passed by draw method is already proteced, we are not worry about change it here. if (Kekule.ObjUtils.isUnset(options.opacity)) options.opacity = configs.getGeneralConfigs().getDrawOpacity(); } return options; */ }, /** * Prepare 2D transform params from baseCoord and drawOptions. * If drawOptions.transformParams already set, this method will do nothing. * @param {Object} context * @param {Hash} baseCoord * @param {Hash} drawOptions * @returns {Hash} */ prepareTransformParams: function(context, baseCoord, drawOptions, objBox) { var result; if (drawOptions.transformParams) { result = drawOptions.transformParams; } else // transform params not calculated, this is the root renderer { // for the root renderer, consider over sampling, change the unit length here if (!drawOptions.unitLength) drawOptions.unitLength = 1; var overSamplingRatio = this.getDrawBridge()._getOverSamplingRatio(context); if (overSamplingRatio && overSamplingRatio !== 1) drawOptions.unitLength *= overSamplingRatio; //console.log('prepareTransformParams', this.getClassName(), drawOptions.unitLength, drawOptions); var p = this.generateTransformParams(context, baseCoord, drawOptions, objBox); drawOptions.transformParams = p; result = p; // since we now have over sampling and autofit, the actual zoom(transformParams.zoom) may differ from initial drawOptions.unitLength, need to feed it back // The drawOptions was already protected and will not affect the one passed to draw() if (drawOptions.transformParams.zoom) drawOptions.zoom = drawOptions.transformParams.zoom; } var transformMatrix = Kekule.CoordUtils.calcTransform2DMatrix(result); var invTransformMatrix = Kekule.CoordUtils.calcInverseTransform2DMatrix(result); drawOptions.transformParams.transformMatrix = transformMatrix; drawOptions.transformParams.invTransformMatrix = invTransformMatrix; this.getRenderCache(context).transformParams = result; this.getRenderCache(context).transformMatrix = transformMatrix; this.getRenderCache(context).invTransformMatrix = invTransformMatrix; //console.log('transform params', drawOptions.transformParams); // also calculate and store the contextRefLength, it may used by child renderers var contextRefLengthes = this.calcContextRefLengthes(context, drawOptions, transformMatrix); drawOptions.contextRefLengthes = { 'x': contextRefLengthes.contextRefLengthX, 'y': contextRefLengthes.contextRefLengthY, 'xy': contextRefLengthes.contextRefLengthXY }; return result; }, /** @private */ calcContextRefLengthes: function(context, drawOptions, transformMatrix) { var refLength = drawOptions.defScaleRefLength; var coord0 = CU.transform2DByMatrix({x: 0, y: 0}, transformMatrix); var coord1 = CU.transform2DByMatrix({x: refLength, y: 0}, transformMatrix); var coord2 = CU.transform2DByMatrix({x: 0, y: refLength}, transformMatrix); var coord3 = CU.transform2DByMatrix({x: refLength, y: refLength}, transformMatrix); return { 'contextRefLengthX': CU.getDistance(coord1, coord0), 'contextRefLengthY': CU.getDistance(coord2, coord0), 'contextRefLengthXY': CU.getDistance(coord3, coord0) / Math.sqrt(2) } }, /** @private */ calcPreferedTransformOptions: function(context, baseCoord, drawOptions, objBox) { var result = {}; //var generalConfigs = this.getRenderConfigs().getGeneralConfigs(); result.allowCoordBorrow = oneOf(drawOptions.allowCoordBorrow, /*generalConfigs.getAllowCoordBorrow(),*/ false); /* var lengthConfigs = this.getRenderConfigs().getLengthConfigs(); var unitLength = drawOptions.unitLength || lengthConfigs.getUnitLength(); result.unitLength = unitLength; */ result.unitLength = drawOptions.unitLength || 1; if (!objBox) { objBox = this.estimateObjBox(context, drawOptions, result.allowCoordBorrow); } if (!objBox) // object is actually empty, use a fake one to avoid exceptions { objBox = {'x1': 0, 'x2': 1, 'y1': 0, 'y2': 1}; } var boxCenter = {'x': (objBox.x1 + objBox.x2) / 2, 'y': (objBox.y1 + objBox.y2) / 2}; var O = Kekule.ObjUtils; if (O.isUnset(drawOptions.translateX) && O.isUnset(drawOptions.translateY)) // if translate is set, baseCoord will be ignored { if (baseCoord) { result.translateX = baseCoord.x - boxCenter.x; result.translateY = baseCoord.y - boxCenter.y; } //console.log('calc translate', baseCoord, boxCenter); } else { result.translateX = drawOptions.translateX || 0; result.translateY = drawOptions.translateY || 0; } result.zoom = drawOptions.zoom || 1; /* var overSamplingRatio = this.getDrawBridge()._getOverSamplingRatio(context); if (overSamplingRatio && overSamplingRatio !== 1) result.zoom *= overSamplingRatio; */ if ((!drawOptions.scale) && (!drawOptions.scaleX) && (!drawOptions.scaleY)) { var defaultDrawScale; // calculate the default scale, /* but in autofit, default scale will not be used, so bypass */ //if (!drawOptions.autofit) { // auto determinate the scale by defBondLength and median of ctab bond length var defDrawRefLength = (oneOf(drawOptions.refDrawLength, this.getAutoScaleRefDrawLength(drawOptions)) || 1) * result.unitLength; //var medianObjRefLength = this.getAutoScaleRefObjLength(this.getChemObj(), result.allowCoordBorrow); var medianObjRefLength = drawOptions.medianObjRefLength; if (Kekule.ObjUtils.isUnset(medianObjRefLength)) medianObjRefLength = drawOptions.defScaleRefLength; defaultDrawScale = (defDrawRefLength / medianObjRefLength) || 1; // medianObjRefLength may be NaN } if (drawOptions.autofit || drawOptions.autoShrink) { var contextDim = this.getDrawBridge().getContextDimension(context); contextDim.x = contextDim.width; contextDim.y = contextDim.height; contextDim = this.getDrawBridge().transformScreenCoordToContext(context, contextDim); contextDim.width = contextDim.x; contextDim.height = contextDim.y; var padding = drawOptions.autofitContextPadding; padding *= result.unitLength * 2; objBox.width = objBox.x2 - objBox.x1; objBox.height = objBox.y2 - objBox.y1; var sx = Math.max(contextDim.width - padding, 0) / (objBox.width || 1); // avoid div by 0 var sy = Math.max(contextDim.height - padding, 0) / (objBox.height || 1); var adjustedScale = Math.min(sx, sy); var adjustedScaleRatio = adjustedScale / defaultDrawScale; if (O.isUnset(drawOptions.retainAspect) || (drawOptions.retainAspect)) { result.scaleX = result.scaleY = defaultDrawScale; // here we should adjust zoom rather than the scaleX/scaleY, since zoom also affects the font size if (drawOptions.autofit) result.zoom *= adjustedScaleRatio; else if (drawOptions.autoShrink) // if adjustedScale > 1, auto shrink will not take effect { if (adjustedScale < defaultDrawScale) result.zoom *= adjustedScaleRatio; } //console.log(result.scaleX, result.unitLength, adjustedScaleRatio); /* if (drawOptions.autofit) result.scaleX = result.scaleY = adjustedScale; else if (drawOptions.autoShrink) // if adjustedScale > 1, auto shrink will not take effect result.scaleX = result.scaleY = ((adjustedScale < defaultDrawScale)? adjustedScale: defaultDrawScale); */ } else { if (drawOptions.autofit) { /* result.scaleX = sx; result.scaleY = sy; */ result.scaleX = ((sx < sy)? 1: sx / sy) * defaultDrawScale; result.scaleY = ((sy < sx)? 1: sy / sx) * defaultDrawScale; result.zoom *= adjustedScaleRatio; } else if (drawOptions.autoShrink) { /* result.scaleX = (sx < defaultDrawScale)? sx: defaultDrawScale; result.scaleY = (sy < defaultDrawScale)? sy: defaultDrawScale; */ result.zoom *= adjustedScaleRatio; if (adjustedScaleRatio < 1) { result.scaleX = result.scaleY = defaultDrawScale; } else { if (sx < sy) { result.scaleX = defaultDrawScale; result.scaleY = defaultDrawScale * sy / sx; } else { result.scaleY = defaultDrawScale; result.scaleX = defaultDrawScale * sx / sy; } } } } } else // if (drawOptions.autoScale) // default is autoScale if no explicit scale set { /* // auto determinate the scale by defBondLength and median of ctab bond length var defDrawRefLength = oneOf(drawOptions.refDrawLength, this.getAutoScaleRefDrawLength(drawOptions)) || 1; //var medianObjRefLength = this.getAutoScaleRefObjLength(this.getChemObj(), result.allowCoordBorrow); var medianObjRefLength = drawOptions.medianObjRefLength; if (Kekule.ObjUtils.isUnset(medianObjRefLength)) medianObjRefLength = drawOptions.defScaleRefLength; result.scaleX = result.scaleY = (defDrawRefLength / medianObjRefLength) || 1; // medianObjRefLength may be NaN */ result.scaleX = result.scaleY = defaultDrawScale; } } else { result.scaleX = oneOf(drawOptions.scaleX, drawOptions.scale, 1); result.scaleY = oneOf(drawOptions.scaleY, drawOptions.scale, 1); } if (O.notUnset(drawOptions.rotateAngle)) result.rotateAngle = drawOptions.rotateAngle; if (O.isUnset(drawOptions.center)) // center not set, use center coord of Ctab { result.center = boxCenter; // rotation center } else result.center = drawOptions.center; // indicate the absolute center of drawn object if (baseCoord) result.drawBaseCoord = baseCoord; else { result.drawBaseCoord = Kekule.CoordUtils.transform2D(boxCenter, result); } return result; }, /** * Calculate the coordinate transform options from drawOptions. * Descendants can override this method. * Note that unit length and zoom is not take into consideration in this method. * @param {Object} context * @param {Hash} baseCoord * @param {Hash} drawOptions * @param {Hash} objBox * @returns {Hash} */ generateTransformParams: function(context, baseCoord, drawOptions, objBox) { var result = this.calcPreferedTransformOptions(context, baseCoord, drawOptions, objBox); //console.log('preferedTransOptions', result); //console.log('calc with input param', baseCoord, drawOptions, objBox); var initialTransformOptions = Object.extend({}, result); result = this.getFinalTransformParams(context, result); result.initialTransformOptions = initialTransformOptions; //console.log('final render params: ', result); return result; }, /** * Calculate the final params for translation. Zoom and unit length are taken into consideration. * @param {Object} context * @param {Hash} transformParams * @returns {Hash} */ getFinalTransformParams: function(context, transformParams) { var result = Object.create(transformParams); if (result.zoom) { result.scaleX *= result.zoom; result.scaleY *= result.zoom; } // Note: usually {0, 0} chem coord is on bottom left, y-direction should be flipped result.scaleY = -result.scaleY; if (result.unitLength) { // the translate and base coord is not related to unitLength! /* result.translateX *= result.unitLength; result.translateY *= result.unitLength; result.drawBaseCoord.x *= result.unitLength; result.drawBaseCoord.y *= result.unitLength; */ } return result; }, /* * Calculate the actual coordinate transform options from baseOptions (drawOptions). * Descendants can override this method. * @param {Object} context * @param {Object} chemObj * @param {Hash} baseCoord * @param {Hash} baseOptions * @returns {Hash} */ /* calcActualTransformOptions: function(context, chemObj, baseCoord, baseOptions) { if (!chemObj) chemObj = this.getChemObj(); var objBox = this.estimateObjBox(context, chemObj, baseOptions); var O = Kekule.ObjUtils; var op = Object.create(baseOptions || {}); var unitLength = this.getRenderConfigs().getLengthConfigs().getUnitLength() || 1; if (op.translateX) op.translateX *= unitLength; if (op.tranlateY) op.translateY *= unitLength; if (baseCoord) { var boxCenter = {'x': (objBox.x1 + objBox.x2) / 2, 'y': (objBox.y1 + objBox.y2) / 2}; op.translateX = (op.translateX || 0) + baseCoord.x - boxCenter.x; op.translateY = (op.translateY || 0) + baseCoord.y - boxCenter.y; } var zoom = op.zoom || 1; if (op.scale) op.scale *= unitLength * zoom; if (op.scaleX) op.scaleX *= unitLength * zoom; if (op.scaleY) op.scaleY *= unitLength * zoom; if ((!op.scale) && (!op.scaleX) && (!op.scaleY) && op.autoScale) { // auto determinate the scale by defBondLength and median of ctab bond length var defDrawRefLength = oneOf(op.refDrawLength, this.getAutoScaleRefDrawLength()); var medianObjRefLength = this.getAutoScaleRefObjLength(chemObj); op.scale = defDrawRefLength / medianObjRefLength * unitLength * zoom; } if (O.isUnset(op.center)) // center not set, use center coord of Ctab { op.center = {}; op.center.x = (objBox.x1 + objBox.x2) / 2; op.center.y = (objBox.y1 + objBox.y2) / 2; } /* if (op.baseOnRootCoord && chemObj.hasCoord2D && chemObj.hasCoord2D()) // consider coord of chemObj { var baseCoord = chemObj.getCoord2D(); op.translateX = (op.translateX || 0) + baseCoord.x; // * op.scale; op.translateY = (op.translateY || 0) + baseCoord.y; // * op.scale; console.log(op.translateX, op.translateY); } *//* return op; } */ /** @ignore */ getRenderFinalTransformParams: function(context) { return this.getRenderCache(context).transformParams; }, /** @ignore */ getRenderInitialTransformOptions: function(context) { var p = this.getRenderFinalTransformParams(context); return p? p.initialTransformOptions: null; }, /** @private */ doTransformCoordToObj: function(context, chemObj, coord) { var matrix = this.getRenderCache(context).invTransformMatrix; return Kekule.CoordUtils.transform2DByMatrix(coord, matrix); }, /** @private */ doTransformCoordToContext: function(context, chemObj, coord) { var matrix = this.getRenderCache(context).transformMatrix; return Kekule.CoordUtils.transform2DByMatrix(coord, matrix); } }); /** * A base class to render text or rich text on context. * @class * @augments Kekule.Render.ChemObj2DRenderer */ Kekule.Render.RichTextBased2DRenderer = Class.create(Kekule.Render.ChemObj2DRenderer, /** @lends Kekule.Render.RichTextBased2DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.RichTextBased2DRenderer', /** @private */ DRAWN_OBJ_FIELD: '__$drawnObj__', /** @constructs */ initialize: function(/*$super, */chemObj, drawBridge, parent) { this.tryApplySuper('initialize', [chemObj, drawBridge, parent]) /* $super(chemObj, drawBridge, parent) */; // flags about size auto recalculation this.__$alwaysRecalcSize__ = false; this.__$isRecalculatingSize = false; }, /** @private */ getDrawnObj: function(context) { return this.getExtraProp(context, this.DRAWN_OBJ_FIELD); }, /** @private */ setDrawnObj: function(context, value) { this.setExtraProp(context, this.DRAWN_OBJ_FIELD, value); }, /** * Returns rich text that need to be drawn of chemObj. * Descendants should override this method. * @param {Kekule.ChemObject} chemObj * @returns {Object} * @private */ getRichText: function(chemObj, drawOptions) { return null; // do nothing here }, /** * Returns a hash object that contains the text draw options. * Descendants should override this method. * @param options * @returns {Hash} * @private */ extractRichTextDrawOptions: function(options) { var result = Object.create(options || {}); result.horizontalAlign = oneOf(result.horizontalAlign, result.textHorizontalAlign); result.verticalAlign = oneOf(result.verticalAlign, result.textVerticalAlign); result.charDirection = oneOf(result.charDirection, result.textCharDirection); return result; }, /** * Returns the actual alignment coord to draw text. * Decendants may override this method. * @param {Hash} baseCoord * @returns {Hash} * @private */ getDrawTextCoord: function(context, baseCoord) { return baseCoord; }, /** @private */ doDrawSelf: function(/*$super, */context, baseCoord, options) { this.tryApplySuper('doDrawSelf', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; //console.log('draw text options', options); var chemObj = this.getChemObj(); var transformOptions = options.transformParams; var richText = this.getRichText(this.getChemObj(), options); if (!richText) return null; if (!baseCoord) baseCoord = this.getAutoBaseCoord(options); var textCoord = this.getDrawTextCoord(context, baseCoord); //console.log('draw text options', Kekule.Render.RichTextUtils.toText(richText), options, this.extractRichTextDrawOptions(options)); var actualRichTextOptions = this.extractRichTextDrawOptions(options); // font size, consider of unitLength if (actualRichTextOptions.fontSize) actualRichTextOptions.fontSize *= (actualRichTextOptions.unitLength || 1); var result = this.drawRichText(context, textCoord, richText, actualRichTextOptions); //console.log('draw text', textCoord, richText, this.extractRichTextDrawOptions(options)); //console.log(result); var rect = result.boundRect; var rectBoundInfo = this.createRectBoundInfo({x: rect.left, y: rect.top}, {x: rect.left + rect.width, y: rect.top + rect.height}); //this.basicDrawObjectUpdated(context, chemObj, chemObj, this.createRectBoundInfo({x: rect.x1, y: rect.y1}, {x: rect.x2, y: rect.y2}), Kekule.Render.ObjectUpdateType.ADD); this.basicDrawObjectUpdated(context, chemObj, chemObj, rectBoundInfo, Kekule.Render.ObjectUpdateType.ADD); this.getRenderCache(context).drawnObj = result.drawnObj; this.setDrawnObj(context, result.drawnObj); // some chem object (e.g. text block) may need to set size automatically when drawing if (this.getCanModifyTargetObj() && (chemObj.getNeedRecalcSize && chemObj.getNeedRecalcSize()) || (this.__$alwaysRecalcSize__)) { this._autosetObjSize(context, chemObj, rectBoundInfo); } return result.drawnObj; }, /** @private */ _autosetObjSize: function(context, chemObj, rectBoundInfo) { if (this.__$isRecalculatingSize) // avoid recursion return; if (chemObj.hasProperty('size2D') && chemObj.setNeedRecalcSize) { this.__$isRecalculatingSize = true; try { var coords = rectBoundInfo.coords; // context coords var objCoord1 = this.transformCoordToObj(context, chemObj, coords[0]); var objCoord2 = this.transformCoordToObj(context, chemObj, 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 chemObj.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__; chemObj.setNeedRecalcSize(false); } finally { this.__$isRecalculatingSize = false; } } } }); /** * A default class to render a text block. * @class * @augments Kekule.Render.RichTextBased2DRenderer */ Kekule.Render.TextBlock2DRenderer = Class.create(Kekule.Render.RichTextBased2DRenderer, /** @lends Kekule.Render.TextBlock2DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.TextBlock2DRenderer', /** @private */ getRichText: function(chemObj, drawOptions) { var result = Kekule.Render.RichTextUtils.strToRichText(chemObj.getText()); return result; }, /** @private */ doEstimateSelfObjBox: function(context, options, allowCoordBorrow) { return this.getChemObj().getBox2D(allowCoordBorrow); }, /** @ignore */ getDrawTextCoord: function(context, baseCoord) { var chemObj = this.getChemObj(); // calc context size of text box var objBox = chemObj.getExposedContainerBox(); var coord1 = {x: objBox.x1, y: objBox.y2}; var coord2 = {x: objBox.x2, y: objBox.y1}; var contextCoord1 = this.transformCoordToContext(context, chemObj, coord1); var contextCoord2 = this.transformCoordToContext(context, chemObj, coord2); var size = Kekule.CoordUtils.substract(contextCoord2, contextCoord1); // since baseCoord is at the center of object, we need calculate out the corner coord to draw text var result = {x: baseCoord.x - size.x / 2, y: baseCoord.y - size.y / 2}; return result; }, /** private */ extractRichTextDrawOptions: function(/*$super, */options) { //var ops = Kekule.Render.RenderOptionUtils.extractRichTextDraw2DOptions(renderConfigs, options || {}); var ops = this.tryApplySuper('extractRichTextDrawOptions', [options]) /* $super(options) */; ops.fontSize = oneOf(ops.fontSize, ops.labelFontSize); ops.fontFamily = oneOf(ops.fontFamily, ops.labelFontFamily); ops.color = oneOf(ops.color, ops.labelColor); ops.textBoxXAlignment = Kekule.Render.BoxXAlignment.LEFT; ops.textBoxYAlignment = Kekule.Render.BoxYAlignment.TOP; return ops; } }); /** * A default class to render a formula. * @class * @augments Kekule.Render.RichTextBased2DRenderer */ Kekule.Render.Formula2DRenderer = Class.create(Kekule.Render.RichTextBased2DRenderer, /** @lends Kekule.Render.Formula2DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.Formula2DRenderer', /** @ignore */ basicDrawObjectUpdated: function(/*$super, */context, obj, parentObj, boundInfo, updateType) { if (obj === this.getChemObj()) { return this.tryApplySuper('basicDrawObjectUpdated', [context, obj.getParent(), obj.getParent(), boundInfo, updateType]) /* $super(context, obj.getParent(), obj.getParent(), boundInfo, updateType) */; // register with molecule, not formula itself } else return this.tryApplySuper('basicDrawObjectUpdated', [context, obj, parentObj, boundInfo, updateType]) /* $super(context, obj, parentObj, boundInfo, updateType) */; }, /** @private */ getRichText: function(chemObj, drawOptions) { return chemObj.getDisplayRichText(true, drawOptions.displayLabelConfigs, drawOptions.partialChargeDecimalsLength, drawOptions.chargeMarkType); // show charge }, /** @private */ doEstimateSelfObjBox: function(context, options, allowCoordBorrow) { var parent = this.getChemObj()? this.getChemObj().getParent(): null; if (parent) { var coord = (parent) ? parent.getAbsBaseCoord2D(allowCoordBorrow) : {'x': 0, 'y': 0}; return BU.createBox(coord, coord); // formula has no box in chem object scope, only a point } else return null; }, /** private */ extractRichTextDrawOptions: