kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,533 lines (1,400 loc) • 176 kB
JavaScript
/**
* @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: