/**
* @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',
/**
* Create a context element for drawing.
* @param {Element} parentElem
* //@param {Int} contextOffsetX X coord of top-left corner of context, in px.
* //@param {Int} contextOffsetY Y coord of top-left corner of context, in px.
* @param {Int} width Width of context, in px.
* @param {Int} height Height of context, in px.
* @returns {Object} Context used for drawing.
*/
createContext: function(parentElem, width, height)
{
return null;
},
/**
* Destroy context created.
* @param {Object} context
*/
releaseContext: function(context)
{
},
/**
* Get width and height of context.
* @param {Object} context
* @returns {Hash} {width, height}
*/
getContextDimension: 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;
},
/**
* 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)
{
},
/**
* 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)
{
},
/**
* 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;
},
/**
* Transform a screen based coord to context based one.
* @param {Object} context
* @param {Hash} coord
* @return {Hash}
*/
transformScreenCoordToContext: function(context, coord)
{
return coord;
},
/**
* 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 and autofit (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). 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)
{
$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()*/);
},
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',
/** @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 $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
{
var p = this.generateTransformParams(context, baseCoord, drawOptions, objBox);
drawOptions.transformParams = p;
result = p;
}
var transformMatrix = Kekule.CoordUtils.calcTransform2DMatrix(result);
var invTransformMatrix = Kekule.CoordUtils.calcInverseTransform2DMatrix(result);
drawOptions.transformParams.transformMatrix = transformMatrix;
drawOptions.transformParams.invTransformMtrix = invTransformMatrix;
this.getRenderCache(context).transformParams = result;
this.getRenderCache(context).transformMatrix = transformMatrix;
this.getRenderCache(context).invTransformMatrix = invTransformMatrix;
//console.log('transform params', drawOptions.transformParams);
return result;
},
/** @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);
}
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;
}
}
else
{
result.translateX = drawOptions.translateX || 0;
result.translateY = drawOptions.translateY || 0;
}
result.zoom = drawOptions.zoom || 1;
if ((!drawOptions.scale) && (!drawOptions.scaleX) && (!drawOptions.scaleY))
{
if (drawOptions.autofit)
{
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);
if (O.isUnset(drawOptions.retainAspect) || (drawOptions.retainAspect))
{
result.scaleX = result.scaleY = Math.min(sx, sy);
}
else
{
result.scaleX = sx;
result.scaleY = 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
}
}
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)
{
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)
{
$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)
{
$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 result = this.drawRichText(context, textCoord, richText,
this.extractRichTextDrawOptions(options));
//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 = $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 $super(context, obj.getParent(), obj.getParent(), boundInfo, updateType); // register with molecule, not formula itself
}
else
return $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: function($super, options)
{
//var ops = Kekule.Render.RenderOptionUtils.extractRichTextDraw2DOptions(renderConfigs, options || {});
var ops = $super(options);
/*
ops.fontSize = oneOf(ops.atomFontSize, ops.fontSize);
ops.fontFamily = oneOf(ops.atomFontFamily, ops.fontFamily);
*/
ops.fontSize = oneOf(ops.fontSize, ops.atomFontSize);
ops.fontFamily = oneOf(ops.fontFamily, ops.atomFontFamily);
ops.color = oneOf(ops.color, ops.labelColor, ops.atomColor);
ops.textBoxXAlignment = Kekule.Render.BoxXAlignment.CENTER;
ops.textBoxYAlignment = Kekule.Render.BoxYAlignment.CENTER;
return ops;
}
});
//Kekule.ClassDefineUtils.addExtraObjMapSupport(Kekule.Render.Formula2DRenderer);
/**
* A default class to render a some text based chem markers.
* @class
* @augments Kekule.Render.RichTextBased2DRenderer
*/
Kekule.Render.TextBasedChemMarker2DRenderer = Class.create(Kekule.Render.RichTextBased2DRenderer,
/** @lends Kekule.Render.TextBasedChemMarker2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.TextBasedChemMarker2DRenderer',
/** @constructs */
initialize: function($super, chemObj, drawBridge, parent)
{
$super(chemObj, drawBridge, parent);
//this.__$alwaysRecalcSize__ = true; // always recalc size of marker block
},
/** @private */
doDrawSelf: function($super, context, baseCoord, options)
{
return $super(context, baseCoord, options);
/* debug
if (!baseCoord)
baseCoord = this.getAutoBaseCoord(options);
var textCoord = this.getDrawTextCoord(context, baseCoord);
this.drawCircle(context, textCoord, 3, {'strokeColor': '#ff0000'});
*/
},
/** @private */
getRichText: function(chemObj, drawOptions)
{
var obj = this.getChemObj();
var text;
if (obj instanceof Kekule.ChemMarker.Charge)
{
var charge = obj.getValue();
text = Kekule.Render.ChemDisplayTextUtils.getChargeDisplayText(charge, drawOptions.partialChargeDecimalsLength, drawOptions.chargeMarkType);
//var section = Kekule.Render.ChemDisplayTextUtils.createElectronStateDisplayTextSection(charge, 0, drawOptions.partialChargeDecimalsLength, drawOptions.chargeMarkType);
if (text)
return Kekule.Render.RichTextUtils.createSection(text,
{'charDirection': Kekule.Render.TextDirection.LTR});
}
else if (obj instanceof Kekule.ChemMarker.Radical)
{
var radical = obj.getValue();
text = Kekule.Render.ChemDisplayTextUtils.getRadicalDisplayText(radical, drawOptions.distinguishSingletAndTripletRadical);
if (text)
return Kekule.Render.RichTextUtils.createSection(text,
{'charDirection': Kekule.Render.TextDirection.LTR});
}
return null;
},
/** @private */
doEstimateSelfObjBox: function($super, context, options, allowCoordBorrow)
{
return $super(context, options, allowCoordBorrow);
//return this.getChemObj().getBox2D(allowCoordBorrow);
},
/** private */
extractRichTextDrawOptions: function($super, options)
{
//var ops = Kekule.Render.RenderOptionUtils.extractRichTextDraw2DOptions(renderConfigs, options || {});
var ops = $super(options);
var obj = this.getChemObj();
ops.fontSize = oneOf(obj.getRenderOption('fontSize'), ops.chemMarkerFontSize, ops.fontSize, ops.atomFontSize);
ops.fontFamily = oneOf(ops.fontFamily, ops.atomFontFamily);
ops.color = oneOf(ops.color, ops.atomColor, ops.labelColor);
ops.textBoxXAlignment = Kekule.Render.BoxXAlignment.CENTER;
ops.textBoxYAlignment = Kekule.Render.BoxYAlignment.CENTER;
return ops;
}
});
/**
* Class to render a image block object.
* @class
* @augments Kekule.Render.ChemObj2DRenderer
*/
Kekule.Render.ImageBlock2DRenderer = Class.create(Kekule.Render.ChemObj2DRenderer,
/** @lends Kekule.Render.ImageBlock2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ImageBlock2DRenderer',
/** @constructs */
initialize: function($super, chemObj, drawBridge, parent)
{
$super(chemObj, drawBridge, parent);
},
/** @private */
doDrawSelf: function($super, context, baseCoord, options)
{
$super(context, baseCoord, options);
//console.log('draw text options', options);
var chemObj = this.getChemObj();
var transformOptions = options.transformParams;
if (!chemObj || !chemObj.getSrc || !chemObj.getSrc())
return null;
if (!baseCoord)
baseCoord = this.getAutoBaseCoord(options);
// calc context size of image
var objBox = chemObj.getExposedContainerBox();
/*
var coord1 = chemObj.getCornerCoord1(Kekule.CoordMode.COORD2D, true);
var coord2 = chemObj.getCornerCoord2(Kekule.CoordMode.COORD2D, true);
*/
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
var drawCoord = {x: baseCoord.x - size.x / 2, y: baseCoord.y - size.y / 2};
//var drawCoord = contextCoord1;
/*
var result = this.drawImage(context, chemObj.getSrc(), baseCoord, size,
options);
*/
var result;
var imgElem = chemObj.getCacheImg();
if (imgElem)
{
result = this.drawImageElem(context, imgElem, drawCoord, size, options);
}
else
result = this.drawImage(context, chemObj.getSrc(), drawCoord, size,
options);
// debug
//var result = this.drawRect(context, contextCoord1, contextCoord2, options);
this.basicDrawObjectUpdated(context, chemObj, chemObj,
this.createRectBoundInfo(drawCoord, Kekule.CoordUtils.add(drawCoord, size)), Kekule.Render.ObjectUpdateType.ADD);
//this.setObjDrawElem(context, chemObj, result);
return result;
}
});
/**
* Class to render a unbonded electron set marker.
* @class
* @augments Kekule.Render.ChemObj2DRenderer
*/
Kekule.Render.UnbondedElectronSetRenderer = Class.create(Kekule.Render.ChemObj2DRenderer,
/** @lends Kekule.Render.UnbondedElectronSetRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.UnbondedElectronSetRenderer',
/** private */
_extractActualDrawOptions: function(options)
{
//var ops = Kekule.Render.RenderOptionUtils.extractRichTextDraw2DOptions(renderConfigs, options || {});
var ops = Object.create(options);
//console.log(ops);
var obj = this.getChemObj();
ops.fontSize = oneOf(obj.getRenderOption('fontSize'), ops.chargeMarkFontSize, ops.fontSize, ops.atomFontSize);
ops.color = oneOf(ops.color, ops.atomColor, ops.labelColor);
ops.strokeColor = ops.color;
ops.fillColor = ops.color;
ops.electronGap = ops.fontSize / 3 * (ops.unitLength || 1) * (ops.zoom || 1); // TODO: currently fixed
ops.electronRadius = ops.fontSize / 12 * (ops.unitLength || 1) * (ops.zoom || 1); // TODO: currently fixed
return ops;
},
/** @private */
_drawSingleElectron: function(context, coord, radius, options)
{
return this.drawCircle(context, coord, radius, options);
},
/** @private */
_drawElectrons: function(context, electronCount, baseCoord, centerCoord, options)
{
//console.log('draw unbonded e', electronCount, baseCoord, centerCoord);
var baseVector = CU.substract(baseCoord, centerCoord);
var distance = CU.getDistance(baseVector);
var cosAngle;
var sinAngle;
if (distance === 0) // avoid zero divided
{
var cosAngle = 0;
var sinAngle = 1;
}
else
{
var cosAngle = baseVector.x / distance;
var sinAngle = baseVector.y / distance;
}
var gap = options.electronGap;
var eRadius = options.electronRadius;
var deltaVector = {'x': gap * sinAngle, 'y': -gap * cosAngle};
var halfCount = (electronCount - 1) / 2;
var currCoord = CU.substract(baseCoord, CU.multiply(deltaVector, halfCount));
var electronCoords = [];
var group = (electronCount > 1)? this.createDrawGroup(context): null;
var drawElem;
for (var i = 0; i < electronCount; ++i)
{
//console.log('draw single e', currCoord, deltaVector);
electronCoords.push(currCoord);
drawElem = this._drawSingleElectron(context, currCoord, eRadius, options);
if (group)
this.addToDrawGroup(drawElem, group);
currCoord = CU.add(currCoord, deltaVector);
}
// boundInfo
var boundInfo;
if (electronCoords.length <= 1) // circle bound
boundInfo = this.createCircleBoundInfo(electronCoords[0], eRadius);
else // line bound
{
var adjustDelta = CU.multiply(deltaVector, eRadius * 2 / gap);
var c1 = CU.substract(electronCoords[0], adjustDelta);
var c2 = CU.add(electronCoords[electronCount - 1], adjustDelta);
boundInfo = this.createLineBoundInfo(c1, c2, eRadius);
}
return {'electronCoords': electronCoords, 'drawnElem': group || drawElem, 'electronRadius': eRadius, 'boundInfo': boundInfo};
},
_doGetParentCoord: function(context, parentObj, options)
{
var transformParams = options.transformParams;
if (transformParams)
{
var obj = parentObj;
var objCoord = obj.getAbsBaseCoord? obj.getAbsBaseCoord(Kekule.CoordMode.COORD2D):
obj.getAbsBaseCoord2D? obj.getAbsBaseCoord2D(): null;
if (objCoord)
{
return CU.transform2D(objCoord, transformParams);
}
}
return null;
},
/** @private */
doDrawSelf: function($super, context, baseCoord, options)
{
$super(context, baseCoord, options);
var ops = this._extractActualDrawOptions(options);
var chemObj = this.getChemObj();
var parentObj = chemObj.getParent();
var transformOptions = options.transformParams;
if (!chemObj || !parentObj || !chemObj.getElectronCount || (chemObj.getElectronCount() || 0) <= 0)
return null;
if (!baseCoord)
baseCoord = this.getAutoBaseCoord(options);
var parentCoord = this._doGetParentCoord(context, parentObj, options);
//console.log('draw lone pair', baseCoord, parentCoord, options);
if (!baseCoord || !parentCoord)
return null;
// do actual draw
var drawResult = this._drawElectrons(context, chemObj.getElectronCount(), baseCoord, parentCoord, ops);
var electronCoords = drawResult.electronCoords;
var radius = drawResult.electronRadius;
/*
var box = Kekule.CoordUtils.getContainerBox(electronCoords);
this.basicDrawObjectUpdated(context, chemObj, chemObj,
this.createRectBoundInfo({x: box.x1, y: box.y1}, {x: box.x2, y: box.y2}), Kekule.Render.ObjectUpdateType.ADD);
console.log(box);
*/
this.basicDrawObjectUpdated(context, chemObj, parentObj, drawResult.boundInfo, Kekule.Render.ObjectUpdateType.ADD);
//this.setObjDrawElem(context, chemObj, result);
var drawnElem = drawResult.drawnElem;
return drawnElem;
}
});
/**
* A default implementation of 2D Ctab (in molecule or path glyph) renderer.
* @class
* @augments Kekule.Render.ChemObj2DRenderer
*/
Kekule.Render.Ctab2DRenderer = Class.create(Kekule.Render.ChemObj2DRenderer,
/** @lends Kekule.Render.Ctab2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.Ctab2DRenderer',
/** @private */
DRAW_ELEM_FIELD: '__$drawElem__',
/** @private */
TRANSFORM_COORD_FIELD: '__$transCoord2D__',
/** @private */
TRANSFORM_MATRIX_FIELD: '__$transMatrix__',
/** private */
RENDERED_OBJS_FIELD: '__$renderedObjs__',
/** @private */
INV_TRANSFORM_MATRIX_FIELD: '__$inverseTransMatrix__',
/** @private */
CHILD_TRANSFORM_MATRIX_FIELD: '__$childTransMatrix__',
/** @private */
doEstimateSelfObjBox: function(context, options, allowCoordBorrow)
{
// TODO: just a rough calc
var box = this.getChemObj().getExposedContainerBox2D(allowCoordBorrow);
return box;
},
/** @private */
getObjDrawElem: function(context, obj)
{
return this.getExtraProp2(context, obj, this.DRAW_ELEM_FIELD);
},
/** @private */
setObjDrawElem: function(context, obj, value)
{
this.setExtraProp2(context, obj, this.DRAW_ELEM_FIELD, value);
},
/** @private */
getRenderedObjs: function(context)
{
return this.getExtraProp(context, this.RENDERED_OBJS_FIELD);
},
/** @private */
addRenderedObj: function(context, obj)
{
//console.log('add rendered obj', obj.getClassName());
var objs = this.getRenderedObjs(context);
if (!objs)
{
objs = [obj];
this.setExtraProp(context, this.RENDERED_OBJS_FIELD, objs);
}
else
{
Kekule.ArrayUtils.pushUnique(objs, obj);
}
},
/** @private */
removeRenderedObj: function(context, obj)
{
//console.log('remove rendered obj', obj.getClassName());
var objs = this.getRenderedObjs(context);
if (objs)
{
Kekule.ArrayUtils.remove(objs, obj);
}
},
/** @private */
clearRenderedObj: function(context)
{
this.setExtraProp(context, this.RENDERED_OBJS_FIELD, null);
},
/** @private */
prepareGeneralOptions: function($super, context, options)
{
return $super(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.useAtomSpecifiedColor))
options.useAtomSpecifiedColor = configs.getColorConfigs().getUseAtomSpecifiedColor();
}
return options;
*/
},
/** @private */
doPrepare: function($super, context, chemObj, baseCoord, options)
{
$super(context, chemObj, baseCoord, options);
},
/** @private */
handleNodeSpecifiedRenderOptions: function(currObj, parentOptions)
{
var localOptions = (currObj.getOverriddenRenderOptions? currObj.getOverriddenRenderOptions(): null) || {};
var result = Object.create(parentOptions);
//var result = Object.extend({}, parentOptions);
result = Object.extend(result, localOptions);
return result;
},
/** @private */
handleConnectorSpecifiedRenderOptions: function($super, currObj, parentOptions)
{
var localOptions = (currObj.getOverriddenRenderOptions? currObj.getOverriddenRenderOptions(): null) || {};
var result = Object.create(parentOptions || null);
result = Object.extend(result, localOptions);
return result;
},
/** @ignore */
isChemObjRenderedBySelf: function($super, context, obj)
{
var renderedObjs = this.getRenderedObjs(context);
var result = $super(context, obj) || (renderedObjs && (renderedObjs.indexOf(obj) >= 0))
|| (this.getChemObj().hasChildObj(obj) && (!obj.isExposed || obj.isExposed()));
//console.log('check if rendered', obj.getClassName(), result);
return result;
},
/** @ignore */
isChemObjRenderedDirectlyBySelf: function($super, context, obj)
{
/*
var chemObj = this.getChemObj();
var parentMol = chemObj.getParent && chemObj.getParent();
console.log(parentMol && parentMol.getId());
*/
var renderedObjs = this.getRenderedObjs(context);
return $super(context, obj) || (renderedObjs && (renderedObjs.indexOf(obj) >= 0)); // || (obj === parentMol);
},
/** @private */
doDrawSelf: function($super, context, baseCoord, options)
{
$super(context, baseCoord, options);
//console.log(options);
var chemObj = this.getChemObj();
this._ctab = chemObj;
//var originDisplayType = this.getMoleculeDisplayType();
/*
if (Kekule.ObjUtils.notUnset(options.moleculeDisplayType))
this.setMoleculeDisplayType(options.moleculeDisplayType);
*/
//var transformOptions = this.calcActualTransformOptions(context, chemObj, baseCoord, options);
//var transformOptions = this.getFinalTransformParams(context, options.transformParams);
var transformOptions = options.transformParams;
this.getRenderCache(context).transformOptions = transformOptions;
this.transformCtabCoords2DToContext(context, chemObj, transformOptions);
this.doPrepare(context, chemObj, baseCoord, options);
this.getRenderCache(context).appliedOptions = options;
var result = this.doDrawCore(context, chemObj, options, transformOptions);
//this.setMoleculeDisplayType(originDisplayType);
return result;
},
/** @private */
doRedraw: function(context)
{
var p = this.getRenderCache(context);
//this.clear(context);
// no need to prepare, draw directly
//console.log('redraw', p.appliedOptions);
return this.doDrawCore(context, this.getChemObj(), p.appliedOptions, p.transformOptions);
//return this.doDraw(context, p.baseCoord, p.options);
},
/** @private */
doDrawCore: function(context, chemObj, options, finalTransformOptions)
{
this.clearRenderedObj(context);
// create a new group to contain whole ctab
var group = this.createDrawGroup(context);
this.doDrawConnectors(context, group, chemObj, options, finalTransformOptions);
this.doDrawNodes(context, group, chemObj, options, finalTransformOptions);
this.setObjDrawElem(context, chemObj, group);
return group;
},
/** @private */
doUpdateSelf: function($super, context, updatedObjDetails, updateType)
{
if (this.canModifyGraphic(context))
{
var r = false;
var T = Kekule.Render.ObjectUpdateType;
switch (updateType)
{
case T.ADD:
r = this.doAddNew(context, updatedObjDetails);
break;
case T.MODIFY:
r = this.doModify(context, updatedObjDetails);
break;
case T.REMOVE:
r = this.doRemove(context, Kekule.Render.UpdateObjUtils._extractObjsOfUpdateObjDetails(updatedObjDetails));
break;
default: // clear
return $super(context, updatedObjDetails, updateType);
}
return r;
}
else
return $super(context, updatedObjs, updateType);
},
/** @private */
doAddNew: function(/*$super,*/ context, updatedObjDetails)
{
if (this.canModifyGraphic())
{
return this.doModify(context, updatedObjDetails);
}
else
//return $super(context, updatedObjs);
return false;
},
/** @private */
doModify: function(/*$super,*/ context, updatedObjDetails)
{
if (this.canModifyGraphic())
{
// find old corresponding element to updatedObj and remove it
/*
var elem = this.getObjDrawElem(updatedObj);
if (elem)
this.doRemoveElem(context, elem);
*/
// debug
/*
{
var updatedObjs = Kekule.Render.UpdateObjUtils._extractObjsOfUpdateObjDetails(updatedObjDetails);
console.log('<modify>', updatedObjs.length, updatedObjs);
}
*/
var objs = this._extractObjsOfUpdateObjDetails(updatedObjDetails);
this.remove(context, objs);
// then update new ones
var chemObj = this.getChemObj();
var group = this.getObjDrawElem(context, chemObj);
var cache = this.getRenderCache(context);
//console.log(this.getClassName(), context.canvas.parentNode, cache);
var options = cache.appliedOptions; //cache.options;
if (objs.indexOf(chemObj) >= 0) // update whole ctab
{
//console.log('<update whole ctab>', chemObj.getParent());
this.draw(context, cache.baseCoord, options);
}
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (!obj.isExposed || obj.isExposed())
{
/*
if (obj === chemObj) // update whole ctab
{
this.draw(context, cache.baseCoord, options);
}
else
*/
{
//console.log('modify', obj.getClassName(), chemObj.hasChildObj(obj));
if (chemObj.hasConnector(obj)) // is connector
{
var ops = this.handleConnectorSpecifiedRenderOptions(obj, options);
this.doDrawConnector(context, group, obj, this.getChemObj(), ops, cache.transformParams || {});
//console.log('draw connector', obj);
}
else if (chemObj.hasNode(obj)) // is node
{
var ops = this.handleNodeSpecifiedRenderOptions(obj, options);
this.doDrawNode(context, group, obj, this.getChemObj(), ops, cache.transformParams);
}
}
}
}
return true;
}
else
{
//return $super(context, updatedObjs);
return false;
}
},
/** @private */
doRemove: function(/*$super,*/ context, removedObjs)
{
if (this.canModifyGraphic())
{
var objs = Kekule.ArrayUtils.toArray(removedObjs);
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
//console.log('remove', obj.getClass(), obj.getId());
// find old corresponding element to updatedObj and remove it
var elem = this.getObjDrawElem(context, obj);
if (elem)
{
var group = this.getObjDrawElem(context, this.getChemObj()); //this.getObjDrawElem(context, chemObj);
this.removeFromDrawGroup(elem, group);
this.removeDrawnElem(context, elem);
}
this.removeRenderedObj(context, obj);
this.removeExtraProp2(context, obj); // delete cached coord and drawElem
//console.log('obj removed', obj.getClassName());
this.basicDrawObjectUpdated(context, obj, this.getChemObj(), null, Kekule.Render.ObjectUpdateType.REMOVE);
}
return true;
}
else
//return $super(context, removedObjs);
return false;
},
/* @private */
/*
doClearSelf: function($super, context)
{
var chemObj = this.getChemObj();
if (this.canModifyGraphic())
{
var drawElem = this.getObjDrawElem(context, chemObj); //this.getObjDrawElem(context, chemObj);
if (drawElem)
this.removeDrawnElem(context, drawElem);
}
else
return $super(context);
},
*/
/** @private */
getCoordTransformOptions: function(context)
{
return this.getRenderCache(context).transformOptions;
},
/**
* Transform each 2D coordinates of objects in CTab to current render space.
* This function should be called before the whole draw phrase.
* @private
*/
transformCtabCoords2DToContext: function(context, ctab, transformOptions)
{
// TODO: note that usually chem structure coord (0, 0) is on bottom left rather than top left
// so a y direction flip is essential
/*
transformOptions.scaleX = transformOptions.scaleX || transformOptions.scale;
transformOptions.scaleY = (transformOptions.scaleY || transformOptions.scale);
*/
var allowCoordBorrow = transformOptions.allowCoordBorrow;
var childTransformOptions = Object.extend({}, transformOptions);
childTransformOptions.centerX = 0;
childTransformOptions.centerY = 0;
var coord;
var coordTransformMatrix = transformOptions.transformMatrix;
//Kekule.CoordUtils.calcTransform2DMatrix(transformOptions);
this.setExtraProp2(context, ctab, this.TRANSFORM_MATRIX_FIELD, coordTransformMatrix);
//var childCoordTransformMatrix = Kekule.CoordUtils.calcTransform2DMatrix(childTransformOptions);
var childCoordTransformMatrix = coordTransformMatrix;
this.setExtraProp2(context, ctab, this.CHILD_TRANSFORM_MATRIX_FIELD, coordTransformMatrix);
// also calc for inversed transform matrix
var invMatrix = transformOptions.invTransformMatrix;
//Kekule.CoordUtils.calcInverseTransform2DMatrix(transformOptions);
//console.log('INV CHECK', Kekule.MatrixUtils.multiply(this._coordTransformMatrix, invMatrix));
this.setExtraProp2(context, ctab, this.INV_TRANSFORM_MATRIX_FIELD, invMatrix);
for (var i = 0, l = ctab.getNodeCount(); i < l; ++i)
{
var node = ctab.getNodeAt(i);
//this.transformObjCoord2D(node, transformOptions, childTransformOptions);
this.transformObjCoord2DToContext(context, node, coordTransformMatrix, childCoordTransformMatrix, allowCoordBorrow);
}
},
/**
* Transform 2D coordinates of node or connector to current render space.
* This function should be called by transformCtabCoords2D before the whole draw phrase.
* @private
*/
transformObjCoord2DToContext: function(context, obj, transformMatrix, childTransformMatrix, allowCoordBorrow)
{
var result;
//if (node && node.getBaseCoord2D)
if (obj && obj.getAbsBaseCoord2D)
{
//coord = node.getBaseCoord2D();
var coord = obj.getAbsBaseCoord2D(allowCoordBorrow);
if (coord)
{
var newCoord = Kekule.CoordUtils.transform2DByMatrix(coord, transformMatrix);
this.setTransformedCoord2D(context, obj, newCoord);
result = newCoord;
//console.log(node[this.TRANSFORM_COORD_FIELD]);
}
if (obj.getNodes) // has child nodes
{
// Done: not handle nested structure yet
for (var i = 0, l = obj.getNodeCount(); i < l; ++i)
this.transformObjCoord2DToContext(context, obj.getNodeAt(i), childTransformMatrix, childTransformMatrix, allowCoordBorrow);
}
}
return result;
},
/**
* Get transformed coord.
* @param {Object} context
* @param {Object} obj
* @private
*/
getTransformedCoord2D: function(context, obj, allowCoordBorrow)
{
if (Kekule.ObjUtils.isUnset(allowCoordBorrow))
{
allowCoordBorrow = this.getRenderCache(context).options.transformParams.allowCoordBorrow;
}
//if (!obj[this.TRANSFORM_COORD_FIELD]) // not transformed yet
var isNode = obj instanceof Kekule.BaseStructureNode;
var nodeParent = obj.getParent();
var isConnectorChildNode = isNode && nodeParent && nodeParent instanceof Kekule.BaseStructureConnector;
var result = isNode && !isConnectorChildNode && this.getExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD);
// IMPORTANT: connector center coord is based on node and should not be cached
if (!result)
{
/*
var transformOptions = this._transformOptions || {};
var childTransformOptions = Object.extend({}, transformOptions);
childTransformOptions.centerX = 0;
childTransformOptions.centerY = 0;
this.transformObjCoord2D(obj, transformOptions, childTransformOptions)
*/
var ctab = this.getChemObj();
var transformMatrix = this.getExtraProp2(context, ctab, this.TRANSFORM_MATRIX_FIELD);
var childTransformMatrix = this.getExtraProp2(context, ctab, this.CHILD_TRANSFORM_MATRIX_FIELD);
if (ctab && (ctab.hasNode(obj, false) || ctab.hasConnector(obj, false))) // is direct child of ctab
{
result = this.transformObjCoord2DToContext(context, obj, transformMatrix, childTransformMatrix, allowCoordBorrow);
}
else // is nested child
{
result = this.transformObjCoord2DToContext(context, obj, childTransformMatrix, childTransformMatrix, allowCoordBorrow);
}
}
//console.log('transformed: ', obj[this.TRANSFORM_COORD_FIELD]);
//return obj[this.TRANSFORM_COORD_FIELD];
//return this.getExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD);
return result;
},
/**
* Set transformed coord.
* @param {Object} context
* @param {Object} obj
* @param {Hash} coord
* @private
*/
setTransformedCoord2D: function(context, obj, coord)
{
//obj[this.TRANSFORM_COORD_FIELD] = coord;
this.setExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD, coord);
},
/** @private */
doTransformCoordToObj: function($super, context, chemObj, coord)
{
var matrix = this.getExtraProp2(context, chemObj, this.INV_TRANSFORM_MATRIX_FIELD);
if (matrix)
return Kekule.CoordUtils.transform2DByMatrix(coord, matrix);
else
return $super(context, chemObj, coord);
},
/** @private */
doTransformCoordToContext: function($super, context, chemObj, coord)
{
var matrix = this.getExtraProp2(context, chemObj, this.TRANSFORM_MATRIX_FIELD);
if (matrix)
return Kekule.CoordUtils.transform2DByMatrix(coord, matrix);
else
return $super(context, chemObj, coord);
},
/**
* Draw all nodes in ctab on context.
* @param {Object} context
* @param {Object} group
* @param {Object} ctab
* @param {Object} options
* @param {Object} finalTransformOptions
* @returns {Object} A rendered object.
* @private
*/
doDrawNodes: function(context, group, ctab, options, finalTransformOptions)
{
//console.log(finalTransformOptions);
/*
var ops = Object.create(options);
ops.color = options.atomColor || options.color; // use atomColor to override color settings
ops.atomColor = null;
ops.fontFamily = options.atomFontFamily || options.fontFamily;
ops.atomFontFamily = null;
ops.fontSize = options.atomFontSize || options.fontSize;
ops.atomFontSize = null;
*/
//var ops = this.handleNodeSpecifiedRenderOptions(ctab, options);
var nodes = ctab.getExposedNodes();
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var ops = this.handleNodeSpecifiedRenderOptions(node, options);
var elem = this.doDrawNode(context, group, node, ctab, ops, finalTransformOptions);
/*
if (elem && group)
this.doAddToGroup(elem, group);
*/
}
},
/**
* Draw a node on context. Descendants need to override this method to do actual drawing.
* @param {Object} context
* @param {Object} group
* @param {Object} node
* @param {Object} parentChemObj
* @param {Object} options
* @param {Object} finalTransformOptions
* @returns {Object} A rendered object.
* @private
*/
doDrawNode: function(context, group, node, parentChemObj, options, finalTransformOptions)
{
// do nothing here
},
/**
* Draw all connectors in ctab on context.
* @param {Object} context
* @param {Object} group
* @param {Object} ctab
* @param {Object} options
* @param {Object} finalTransformOptions
* @returns {Object} A rendered object.
* @private
*/
doDrawConnectors: function(context, group, ctab, options, finalTransformOptions)
{
/*
var ops = Object.create(options);
ops.color = options.bondColor || options.color; // use atomColor to override color settings
ops.bondColor = null;
*/
//var ctabOps = this.handleConnectorSpecifiedRenderOptions(ctab, options);
var connectors = ctab.getExposedConnectors();
for (var i = 0, l = connectors.length; i < l; ++i)
{
var connector = connectors[i];
var ops = this.handleConnectorSpecifiedRenderOptions(connector, options);
var elem = this.doDrawConnector(context, group, connector, ctab, ops, finalTransformOptions);
}
},
/**
* Draw a connector on context.
* @param {Object} context
* @param {Object} group
* @param {Object} connector
* @param {Object} options
* @param {Object} finalTransformOptions
* @returns {Object} A rendered object.
* @private
*/
doDrawConnector: function(context, group, connector, parentChemObj, options, finalTransformOptions)
{
var result;
// draw lines between every two connected objects
//var objCount = connector.getConnectedObjCount();
var objs = connector.getConnectedExposedObjs();
var objCount = objs.length;
if (objCount < 2) // less than two connected object, can not draw
{
return null;
}
var obj1, obj2;
var coord1, coord2;
var elem;
var subGroup = (objCount > 2)? this.createDrawGroup(context): null;
var elemEx;
var boundInfos = (objCount > 2)? []: null;
/*
var renderOptions = Kekule.Render.RenderOptionUtils.mergeObjLocalRenderOptions(connector, options);
renderOptions.color = renderOptions.bondColor || renderOptions.color;
*/
//var renderOptions = this.handleConnectorSpecifiedRenderOptions(connector, options);
var renderOptions = options;
for (var i = 0; i < objCount; ++i)
{
obj1 = objs[i];
coord1 = this.getTransformedCoord2D(context, obj1, finalTransformOptions.allowCoordBorrow);
if (coord1)
{
for (var j = i + 1; j < objCount; ++j) // do not draw on self
{
obj2 = objs[j];
coord2 = this.getTransformedCoord2D(context, obj2, finalTransformOptions.allowCoordBorrow);
if (coord2)
{
//console.log('draw connector', coord1, coord2);
elemEx = this.doDrawConnectorShape(context, connector, [obj1, obj2], renderOptions, finalTransformOptions);
if (elemEx)
{
elem = elemEx.element;
//var elem = this.doDrawConnectorLine(context, coord1, coord2, 1, style);
if (elem && subGroup)
this.addToDrawGroup(elem, subGroup);
//return elem;
var boundInfo = elemEx.boundInfo;
if (boundInfos && boundInfo)
boundInfos.push(boundInfo);
}
}
}
}
}
result = subGroup || elem;
if (result && group)
this.addToDrawGroup(result, group);
if (result)
{
//console.log(result);
this.setObjDrawElem(context, connector, result);
//this.basicObjectDrawn(connector, boundInfos || boundInfo);
}
else
this.setObjDrawElem(context, connector, null);
this.addRenderedObj(context, connector);
if (boundInfo || boundInfos) // has bound info, connector is actually drawn
this.basicDrawObjectUpdated(context, connector, parentChemObj, boundInfos || boundInfo, Kekule.Render.ObjectUpdateType.ADD);
return result;
},
/**
* Draw a connector connecting nodes on context. Descendants need to override this method to do actual drawing.
* @param {Object} context
* @param {Kekule.ChemStructureConnector} connector
* @param {Array} nodes
* @param {Hash} renderOptions
* @param {Hash} finalTransformOptions
* @private
*/
doDrawConnectorShape: function(context, connector, nodes, renderOptions, finalTransformOptions)
{
// do nothing here
}
});
//Kekule.ClassDefineUtils.addExtraObjMapSupport(Kekule.Render.Ctab2DRenderer);
Kekule.ClassDefineUtils.addExtraTwoTupleObjMapSupport(Kekule.Render.Ctab2DRenderer);
/**
* A default implementation of 2D a molecule's CTab renderer.
* @class
* @augments Kekule.Render.Ctab2DRenderer
*/
Kekule.Render.ChemCtab2DRenderer = Class.create(Kekule.Render.Ctab2DRenderer,
/** @lends Kekule.Render.ChemCtab2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ChemCtab2DRenderer',
/** @private */
OBJ_NEED_LABEL_FIELD: '__$needDrawLabel__',
/** @private */
OBJ_NEED_DOT_FIELD: '__$needDrawDot__',
//* @private */
//OBJ_HIDDEN_FIELD: '__$hidden__',
/*
* Note: param passing to this function may be node or connector.
* @private
*/
/*
getObjHidden: function(context, obj)
{
var result = this.getExtraProps(context, obj, this.OBJ_HIDDEN_FIELD);
if (Kekule.ObjUtils.isUnset(result))
{
var drawOptions = this.getRenderCache(context).appliedOptions;
result = this._needObjHidden(obj, drawOptions);
//this.setExtraProp2(context, obj, this.OBJ_NEED_LABEL_FIELD, result);
this.setObjNeedDrawLabel(context, obj, result);
}
return result;
},
*/
/*
* @private
*/
/*
setObjHidden: function(context, obj, value)
{
this.setExtraProp2(context, obj, this.OBJ_HIDDEN_FIELD, value);
},
*/
/**
* Note: param passing to this function may be node or connector.
* @private
*/
getObjNeedDrawLabel: function(context, obj)
{
var result = this.getExtraProp2(context, obj, this.OBJ_NEED_LABEL_FIELD);
if (Kekule.ObjUtils.isUnset(result))
{
var drawOptions = this.getRenderCache(context).appliedOptions;
result = (obj instanceof Kekule.ChemStructureConnector)? false: this._needNodeDrawLabel(obj, drawOptions);
//this.setExtraProp2(context, obj, this.OBJ_NEED_LABEL_FIELD, result);
this.setObjNeedDrawLabel(context, obj, result);
}
return result;
},
/**
* @private
*/
setObjNeedDrawLabel: function(context, obj, value)
{
this.setExtraProp2(context, obj, this.OBJ_NEED_LABEL_FIELD, value);
},
/**
* Note: param passing to this function may be node or connector.
* @private
*/
getObjNeedDrawDot: function(context, obj)
{
/*
var result = this.getExtraProp2(context, obj, this.OBJ_NEED_DOT_FIELD);
if (Kekule.ObjUtils.isUnset(result))
{
var drawOptions = this.getRenderCache(context).appliedOptions;
result = (obj instanceof Kekule.ChemStructureConnector)? false: this._needNodeDrawDot(context, obj, drawOptions);
//this.setExtraProp2(context, obj, this.OBJ_NEED_LABEL_FIELD, result);
this.setObjNeedDrawDot(context, obj, result);
}
*/
var drawOptions = this.getRenderCache(context).appliedOptions;
var result = (obj instanceof Kekule.ChemStructureConnector)? false: this._needNodeDrawDot(context, obj, drawOptions);
return result;
},
/** @private */
doPrepare: function($super, context, chemObj, baseCoord, options)
{
$super(context, chemObj, baseCoord, options);
/*
// generate draw options
var c = this.getRenderCache(context);
this._richTextDrawOptions = this.generateRichTextDrawOptions(this.getRenderConfigs(), options);
this._chargeDrawOptions = this.generateChargeDrawOptions(this.getRenderConfigs(), options);
this._connectorDrawOptions = this.generateConnectorDrawOptions(this.getRenderConfigs(), options);
*/
/*
c.richTextDrawOptions = options; //this.generateRichTextDrawOptions(this.getRenderConfigs(), options);
c.chargeDrawOptions = options; //this.generateChargeDrawOptions(this.getRenderConfigs(), options);
c.connectorDrawOptions = options; this.generateConnectorDrawOptions(this.getRenderConfigs(), options);
*/
//this._drawOptions = options;
if (!options.fontSize)
//if (options.atomFontSize)
options.fontSize = options.atomFontSize;
// for drawing atom labels
options.textBoxXAlignment = Kekule.Render.BoxXAlignment.CENTER;
options.textBoxYAlignment = Kekule.Render.BoxYAlignment.CENTER;
//this.getRenderCache(context).appliedOptions = options;
// iterate through nodes to see whether node label need to be set
var nodes = chemObj.getExposedNodes();
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var needDrawLabel = this._needNodeDrawLabel(node, options);
//node[this.NODE_NEED_LABEL_FIELD] = needDrawLabel;
this.setObjNeedDrawLabel(context, node, needDrawLabel);
}
},
/*
* @private
* @deprecated
*/
/*
generateRichTextDrawOptions: function(renderConfigs, inheritedRenderOptions)
{
var ops = Kekule.Render.RenderOptionUtils.extractRichTextDraw2DOptions(renderConfigs, inheritedRenderOptions || {});
ops.textBoxXAlignment = Kekule.Render.BoxXAlignment.CENTER;
ops.textBoxYAlignment = Kekule.Render.BoxYAlignment.CENTER;
return ops;
},
*/
/*
* @private
* @deprecated
*/
/*
generateChargeDrawOptions: function(renderConfigs, inheritedRenderOptions)
{
var inherited = inheritedRenderOptions || {};
var op = {
'showCharge': true,
'chargeMarkType': oneOf(inherited.chargeMarkType, renderConfigs.getMoleculeDisplayConfigs().getDefChargeMarkType()),
'chargeMarkFontSize': oneOf(inherited.chargeMarkFontSize, renderConfigs.getLengthConfigs().getChargeMarkFontSize()),
'chargeMarkMargin': oneOf(inherited.chargeMarkMargin, renderConfigs.getLengthConfigs().getChargeMarkMargin()),
'chargeMarkCircleWidth': oneOf(inherited.chargeMarkCircleWidth, renderConfigs.getLengthConfigs().getChargeMarkCircleWidth()),
'partialChargeDecimalsLength': oneOf(inherited.partialChargeDecimalsLength, renderConfigs.getMoleculeDisplayConfigs().getPartialChargeDecimalsLength()),
'color': oneOf(inherited.atomColor, inherited.color, renderConfigs.getColorConfigs().getAtomColor()),
'opacity': oneOf(inherited.opacity, renderConfigs.getGeneralConfigs().getDrawOpacity()),
'unitLength': oneOf(inherited.unitLength, renderConfigs.getLengthConfigs().getUnitLength())
};
return op;
},
*/
/*
* @private
* @deprecated
*/
/*
generateConnectorDrawOptions: function(renderConfigs, inheritedRenderOptions)
{
var inherited = inheritedRenderOptions || {};
var op = renderConfigs.getLengthConfigs().toHash();
op.color = oneOf(inherited.bondColor, inherited.color, renderConfigs.getColorConfigs().getBondColor());
op.opacity = oneOf(inherited.opacity, renderConfigs.getGeneralConfigs().getDrawOpacity());
op.atomLabelBoxExpandRatio = oneOf(inherited.atomLabelBoxExpandRatio, renderConfigs.getLengthConfigs().getAtomLabelBoxExpandRatio());
return op;
},
*/
/** @private */
handleNodeSpecifiedRenderOptions: function($super, currObj, parentOptions)
{
var result = $super(currObj, parentOptions);
/*
// color
//result.atomColor = oneOf(localOptions.atomColor, localOptions.color, result.atomColor, result.color);
//result.atomColor = oneOf(localOptions.atomColor, result.atomColor);
//result.color = result.atomColor;
result.color = result.color || result.atomColor;
// font
//result.atomFontFamily = oneOf(localOptions.atomFontFamily, localOptions.fontFamily, result.atomFontFamily, result.fontFamily);
//result.atomFontFamily = oneOf(localOptions.atomFontFamily, result.atomFontFamily);
//console.log(localOptions.atomFontFamily, localOptions.fontFamily, result.atomFontFamily, result.fontFamily)
//result.fontFamily = result.atomFontFamily || result.fontFamily;
result.fontFamily = result.fontFamily || result.atomFontFamily;
//result.atomFontSize = oneOf(localOptions.atomFontSize, localOptions.fontSize, result.atomFontSize, result.fontSize);
//result.atomFontSize = oneOf(localOptions.atomFontSize, result.atomFontSize);
//result.fontSize = result.atomFontSize || result.fontSize;
result.fontSize = result.fontSize || result.atomFontSize;
//console.log(result.atomFontSize, result.fontSize, localOptions, parentOptions);
*/
if (!result.color && result.atomColor)
result.color = result.atomColor;
if (!result.fontFamily && result.atomFontFamily)
result.fontFamily = result.atomFontFamily;
if (!result.fontSize && result.atomFontSize)
result.fontSize = result.atomFontSize;
return result;
},
/** @private */
handleConnectorSpecifiedRenderOptions: function($super, currObj, parentOptions)
{
/*
var localOptions = (currObj.getOverriddenRenderOptions? currObj.getOverriddenRenderOptions(): null) || {};
var result = Object.create(parentOptions);
result = Object.extend(result, localOptions);
*/
var result = $super(currObj, parentOptions);
// color
//result.bondColor = oneOf(localOptions.bondColor, localOptions.color, result.bondColor, result.color);
//result.bondColor = oneOf(localOptions.bondColor, result.bondColor);
//result.color = result.bondColor;
//result.color = result.color || result.bondColor;
if (!result.color && result.bondColor)
{
result.color = result.bondColor;
}
return result;
},
/*
* Check if an object (connector or node) should be hidden from context.
* @param {Object} obj
* @param {Object} drawOptions
* @returns {Bool}
* @private
*/
/*
_needObjHidden: function(context, obj, drawOptions)
{
if (obj instanceof Kekule.ChemStructureConnector)
return this._needConnectorHidden(obj, drawOptions);
else
return this._needNodeHidden(obj, drawOptions);
},
_needNodeHidden: function(context, node, drawOptions)
{
// H should be hidden in implict hydrogen display mode
var r = (node instanceof Kekule.Atom)
&& (node.getAtomicNumber() === 1)
&& (node.getMassNumber() === 1)
&& (!node.getCharge());
if (r)
{
var hDisplayLevel = this._getNodeHydrogenDisplayLevel(node, drawOptions.moleculeDisplayType);
var HDL = Kekule.Render.HydrogenDisplayLevel;
}
},
_needConnectorHidden: function(context, connector, drawOptions)
{
var r = this._isSimpleDefaultBond(connector);
if (r)
{
// one end is hidden, the bond should also be hidden
for (var i = 0, l = connector.getConnectedObjCount(); i < l;++i)
{
var obj = connector.getConnectedObjAt(i);
if (this.getObjHidden(context, obj))
{
r = true;
break;
}
}
}
return r;
},
_isSimpleDefaultBond: function(connector)
{
var r = connector instanceof Kekule.Bond;
if (r)
{
r = (connector.getBondType() === Kekule.BondType.DEFAULT)
&& (connector.getBondOrder() === Kekule.BondOrder.DEFAULT)
&& (connector.getConnectedObjCount() === 2);
}
return r;
},
*/
/**
* Draw a node on context.
* @param {Object} context
* @param {Object} group
* @param {Object} node
* @param {Object} parentChemObj
* @param {Object} options
* @param {Object} finalTransformOptions
* @returns {Object} A rendered object.
* @private
*/
doDrawNode: function(context, group, node, parentChemObj, options, finalTransformOptions)
{
var result;
var boundInfo;
var coord = this.getTransformedCoord2D(context, node, finalTransformOptions.allowCoordBorrow);
//var renderConfigs = this.getRenderConfigs();
//var nodeRenderOptions = this.handleNodeSpecifiedRenderOptions(node, options);
var nodeRenderOptions = options;
//var nodeRenderOptions = Kekule.Render.RenderOptionUtils.mergeObjLocalRenderOptions(node, options);
//var richTextDrawOptions = Object.create(this.getRenderCache(context).richTextDrawOptions);
var atomicNumber = node.getAtomicNumber? node.getAtomicNumber(): 0;
var localOptions = node.getOverriddenRenderOptions() || {};
//var localColor = localOptions.atomColor || localOptions.color;
var localColor = localOptions.color || localOptions.atomColor;
if (localColor)
nodeRenderOptions.color = localColor;
else
{
if (nodeRenderOptions.useAtomSpecifiedColor)
{
var atomSpecifiedColor;
if (atomicNumber <= 0) // not a real atom, may be subgroup or peseudo atom, etc.
{
var atomTypeName = node.getClassLocalName();
atomSpecifiedColor = Kekule.Render.RenderColorUtils.getColor(atomTypeName, this.getRendererType());
}
else
atomSpecifiedColor = Kekule.Render.RenderColorUtils.getColor(atomicNumber, this.getRendererType());
nodeRenderOptions.color = atomSpecifiedColor;
}
}
/*
var defColor = localColor ||
(nodeRenderOptions.useAtomSpecifiedColor?
atomSpecifiedColor:
nodeRenderOptions.color);
//console.log(defColor);
nodeRenderOptions.color = defColor;
*/
/*
nodeRenderOptions.fontFamily = nodeRenderOptions.atomFontFamily || nodeRenderOptions.fontFamily;
nodeRenderOptions.fontSize = nodeRenderOptions.atomFontSize || nodeRenderOptions.fontSize;
*/
/*
var localLabelDrawOptions = Object.create(options);
localLabelDrawOptions = Object.extend(localLabelDrawOptions, Kekule.Render.RenderOptionUtils.getNodeLabelDrawOptions(node.getRenderOptions()));
if (localLabelDrawOptions)
{
richTextDrawOptions = Object.extend(richTextDrawOptions, localLabelDrawOptions);
}
*/
//if (!this._needNodeDrawLabel(node))
//var autoCreateChargeAndRadicalMarker = nodeRenderOptions.autoCreateChargeAndRadicalMarker;
// TODO: charge and radical drawing are now handled togather, may be splitted in the future
var hasChargeOrRadical = node.getCharge() || node.getRadical();
var needDrawCharge = (node.getCharge() && !node.fetchChargeMarker(false));
var needDrawRadical = (node.getRadical() && !node.fetchRadicalMarker(false));
var nodeWithLabel = false;
if (this.getObjNeedDrawLabel(context, node)) // draw label
{
nodeWithLabel = true;
/*
var localOptions = node.getRenderOptions() || {};
var renderOptions = Object.extend(renderConfigs, localOptions);
*/
// if a label is drawn, all hydrogens should be marked
var hdisplayLevel = Kekule.Render.HydrogenDisplayLevel.ALL; //this._getNodeHydrogenDisplayLevel(node);
//console.log(hdisplayLevel);
var needShowChargeInLabel = !!(needDrawCharge || needDrawRadical);
//console.log(node.getCharge(), node.getRadical(), needDrawCharge, needDrawRadical, needShowChargeInLabel);
var label = node.getDisplayRichText(hdisplayLevel, needShowChargeInLabel, nodeRenderOptions.displayLabelConfigs /*renderConfigs.getDisplayLabelConfigs()*/, nodeRenderOptions.partialChargeDecimalsLength, nodeRenderOptions.chargeMarkType, nodeRenderOptions.distinguishSingletAndTripletRadical);
// decide charDirection
//label.charDirection = Kekule.ObjUtils.isUnset(nodeRenderOptions.charDirection) ? this._decideNodeLabelCharDirection(context, node) : nodeRenderOptions.charDirection;
//console.log('nodeCharDirection', nodeRenderOptions.charDirection);
//label.charDirection = !nodeRenderOptions.charDirection ? this._decideNodeLabelCharDirection(context, node) : nodeRenderOptions.charDirection;
var labelCharDirection = !nodeRenderOptions.charDirection ? this._decideNodeLabelCharDirection(context, node) : nodeRenderOptions.charDirection;
// recalc font size to px
//richTextDrawOptions.fontSize *= localLabelDrawOptions.unitLength || renderConfigs.getLengthConfigs().getUnitLength();
nodeRenderOptions.fontSize *= nodeRenderOptions.unitLength;
//console.log('font size', nodeRenderOptions.fontSize);
//console.log('drawLabel', label);
/*
if (nodeRenderOptions.textBoxXAlignment !== Kekule.Render.BoxXAlignment.CENTER)
console.log('draw node label', nodeRenderOptions);
*/
//console.log('draw node label', nodeRenderOptions, label);
var actualDrawOptions = Object.create(nodeRenderOptions);
actualDrawOptions.charDirection = labelCharDirection;
var elemEx = this.drawRichText(context, coord, label,
actualDrawOptions/*nodeRenderOptions/*richTextDrawOptions*/);
var elem = elemEx.drawnObj;
var rect = elemEx.boundRect;
// change boundInfo to a rect
boundInfo = this.createRectBoundInfo({x: rect.left, y: rect.top}, {x: rect.left + rect.width, y: rect.top + rect.height});
//console.log(rect);
result = elem;
}
else
{
var nodeCoreElem, chargeElem;
if (this.getObjNeedDrawDot(context, node) && Kekule.ObjUtils.isUnset(nodeRenderOptions.atomRadius))
{
nodeRenderOptions.atomRadius = nodeRenderOptions.allenCenterAtomRadius;
}
if (nodeRenderOptions.atomRadius)
{
var radius = nodeRenderOptions.atomRadius * nodeRenderOptions.unitLength;
nodeRenderOptions.strokeColor = nodeRenderOptions.color;
nodeRenderOptions.fillColor = nodeRenderOptions.color;
nodeCoreElem = this.drawCircle(context, coord, radius, nodeRenderOptions);
boundInfo = this.createCircleBoundInfo(coord, radius);
}
else
boundInfo = this.createPointBoundInfo(coord);
//if (node.getCharge() || node.getRadical()) // draw charge or radical
if (needDrawCharge || needDrawRadical)
{
/*
var chargeOptions = this.getRenderCache(context).chargeDrawOptions;
var localOptions = Kekule.Render.RenderOptionUtils.getChargeDrawOptions(node.getRenderOptions());
var chargeOptions = Object.extend(chargeOptions, localOptions);
var elemEx = this.doDrawElectronStateMark(context, group, node,
chargeOptions.chargeMarkType,
richTextDrawOptions.fontFamily,
chargeOptions.chargeMarkFontSize * chargeOptions.unitLength,
chargeOptions.chargeMarkMargin * chargeOptions.unitLength,
chargeOptions.chargeMarkCircleWidth * chargeOptions.unitLength,
chargeOptions.color, chargeOptions.opacity);
*/
//if (!autoCreateChargeAndRadicalMarker)
{
var elemEx = this.doDrawElectronStateMark(context, group, node,
nodeRenderOptions.chargeMarkType,
nodeRenderOptions.partialChargeDecimalsLength,
nodeRenderOptions.distinguishSingletAndTripletRadical,
nodeRenderOptions.fontFamily,
nodeRenderOptions.chargeMarkFontSize * nodeRenderOptions.unitLength,
nodeRenderOptions.chargeMarkMargin * nodeRenderOptions.unitLength,
nodeRenderOptions.chargeMarkCircleWidth * nodeRenderOptions.unitLength,
nodeRenderOptions.color, nodeRenderOptions.opacity, nodeRenderOptions.zoom || 1);
if (elemEx)
chargeElem = elemEx.drawnObj;
}
}
if (nodeCoreElem && chargeElem)
{
result = this.createDrawGroup(context);
this.addToDrawGroup(result, nodeCoreElem);
this.addToDrawGroup(result, chargeElem);
}
else
result = nodeCoreElem || chargeElem;
}
//if (hasChargeOrRadical) // if has charge or radical marker, adjust their position if needed
if (node.getUnplacedMarkers(this.getCoordMode()).length >= 0)
{
var nodeLabelDirAngle;
// calc margin
var margin = nodeRenderOptions.chemMarkerMargin;
// consider node label if exists
if (nodeWithLabel)
{
var expandRatio = /*nodeRenderOptions.atomLabelBoxExpandRatio ||*/ 1;
var fSize = this._getNodeFontSize(context, node) * (finalTransformOptions.zoom || 1);
var halfBoxWidth = fSize * nodeRenderOptions.unitLength * expandRatio / 2;
margin += halfBoxWidth - margin / 2; // shrink margin a little for better outlook
//console.log('expand', expandRatio, fSize, halfBoxWidth, margin);
// consider label direction
var D = Kekule.Render.TextDirection;
var directions = [D.LTR, D.RTL, D.TTB, D.BTT];
var labelVector = (labelCharDirection === D.LTR)? {x: 1, y: 0}:
(labelCharDirection === D.RTL)? {x: -1, y: 0}:
(labelCharDirection === D.TTB)? {x: 0, y: 1}:
(labelCharDirection === D.BTT)? {x: 0, y: -1}:
{x: 0, y: 0};
var revTransLabelVector = {
'x': labelVector.x * finalTransformOptions.scaleX,
'y': labelVector.y * finalTransformOptions.scaleY
};
nodeLabelDirAngle = (revTransLabelVector.x > 0)? 0:
(revTransLabelVector.x < 0)? Math.PI:
(revTransLabelVector.y > 0)? Math.PI / 2:
(revTransLabelVector.y < 0)? Math.PI * 3 / 2:
null;
//console.log(labelCharDirection, nodeLabelDirAngle * 180 / Math.PI, labelVector, revTransLabelVector);
}
var objMargin = margin / nodeRenderOptions.defBondLength * nodeRenderOptions.medianObjRefLength;
this.doAdjustChemPropMarkerPos(node, objMargin, finalTransformOptions.allowCoordBorrow,
Kekule.ObjUtils.notUnset(nodeLabelDirAngle)? [nodeLabelDirAngle]: []); // consider label direction
}
if (result)
{
this.setObjDrawElem(context, node, result);
if (group)
this.addToDrawGroup(result, group);
}
else
this.setObjDrawElem(context, node, null);
this.addRenderedObj(context, node);
this.basicDrawObjectUpdated(context, node, parentChemObj, boundInfo, Kekule.Render.ObjectUpdateType.ADD);
//console.log(boundInfo);
return result;
},
/**
* Get font size of a node. If no specified size set in renderOptions, global size will be used.
* @private
*/
_getNodeFontSize: function(context, node, drawOptions)
{
if (!drawOptions)
drawOptions = this.getRenderCache(context).appliedOptions || this.getRenderCache(context).options;
var localLabelDrawOptions = Kekule.Render.RenderOptionUtils.getNodeLabelDrawOptions(node.getOverriddenRenderOptions()) || {};
//return oneOf(localLabelDrawOptions.atomFontSize, localLabelDrawOptions.fontSize, drawOptions.atomFontSize, drawOptions.fontSize);
return oneOf(localLabelDrawOptions.fontSize, drawOptions.fontSize, drawOptions.atomFontSize);
},
/**
* Get hydrogen display level of a node. If no specified size set in renderOptions, global setting will be used.
* @private
*/
_getNodeHydrogenDisplayLevel: function(node, drawOptions)
{
//if (this.getMoleculeDisplayType() === Kekule.Render.MoleculeDisplayType.CONDENSED) // condensed, need display all hydrogens defaultly
var localRenderOptions = node.getOverriddenRenderOptions();
var localLevel = Kekule.Render.RenderOptionUtils.getHydrogenDisplayLevel(localRenderOptions);
var hdisplayLevel = Kekule.ObjUtils.notUnset(localLevel)?
localLevel:
((drawOptions.moleculeDisplayType === Kekule.Render.MoleculeDisplayType.CONDENSED)?
Kekule.Render.HydrogenDisplayLevel.ALL:
drawOptions.hydrogenDisplayLevel);
//this.getRenderConfigs().getMoleculeDisplayConfigs().getDefHydrogenDisplayLevel());
return hdisplayLevel;
},
/**
* Check if a node is carbon atom between two multiple (double) bonds,
* so that a explicit dot should be drawn to avoid confusions in outlook.
* @param {Object} context
* @param {Object} node
* @param {Hash} drawOptions
* @private
*/
_needNodeDrawDot: function(context, node, drawOptions)
{
var renderOps = node.getOverriddenRenderOptions();
if (!(node instanceof Kekule.Atom))
return false;
else if (node.getAtomicNumber() !== Kekule.Render.DEF_ATOM_ATOMIC_NUM)
return false;
else // C atom
{
if (this.getObjNeedDrawLabel(context, node))
return false;
var connectors = node.getLinkedBonds();
if (connectors.length === 2)
{
var bonds = node.getLinkedMultipleBonds();
if (bonds.length === 2)
{
// we have two multiple bonds
if (bonds[0].getBondOrder() === bonds[1].getBondOrder())
{
var bondOrder = bonds[0].getBondOrder();
return bondOrder >= BO.DOUBLE && bondOrder < BO.EXPLICIT_AROMATIC;
}
}
}
return false;
}
},
/**
* Check if a node label should be drawn.
* @param {Object} node
* @private
*/
_needNodeDrawLabel: function(node, drawOptions)
{
var U = Kekule.Render.RenderOptionUtils;
var NM = Kekule.Render.NodeLabelDisplayMode;
var renderOps = node.getOverriddenRenderOptions();
var nodeLabelMode = U.getNodeDisplayMode(renderOps)
|| drawOptions.nodeDisplayMode;
//|| U.getNodeDisplayMode(/*this.getInheritedRenderOptions()*/drawOptions);
//|| this.getRenderConfigs().getMoleculeDisplayConfigs().getDefNodeDisplayMode();
if (!(node instanceof Kekule.ChemStructureNode))
return false;
if (nodeLabelMode === NM.HIDDEN)
return false;
else if (nodeLabelMode === NM.SHOWN)
return true;
else // smart
{
var molDisplayType = drawOptions.moleculeDisplayType; //this.getMoleculeDisplayType();
if (molDisplayType === Kekule.Render.MoleculeDisplayType.CONDENSED)
return true;
else // bond-line
{
var connectors = node.getLinkedConnectors();
if (connectors.length <= 0) // no connectors connected, a standalone node, label must be drawn
return true;
if ((node instanceof Kekule.Atom)
&& (node.getAtomicNumber() === Kekule.Render.DEF_ATOM_ATOMIC_NUM)
&& (!node.getMassNumber())) // is a normal C atom
{
/*
if ((node.getCharge() > 1) || (node.getCharge() < -1)) // has more than one charge, show label defaultly
return true;
*/
// if custom label is set, must shown
if (node.getRenderOption('customLabel'))
return true;
var hDisplayLevel = this._getNodeHydrogenDisplayLevel(node, drawOptions);
var HDL = Kekule.Render.HydrogenDisplayLevel;
if ((hDisplayLevel === HDL.ALL) || (hDisplayLevel === HDL.NONE))
{
return true;
}
else if (hDisplayLevel === HDL.UNMATCHED_EXPLICIT)
{
return (node.getImplicitHydrogenCount() !== node.getExplicitHydrogenCount());
}
else // explicit
return Kekule.ObjUtils.notUnset(node.getExplicitHydrogenCount());
}
else // other atoms or groups need label draw
return true;
}
}
},
/**
* Check if there are connectors to some direction in node.
* @param {Array} linkedObjCoords
* @param {Int} direction Value from {@link Kekule.Render.TextDirection}.
* @param {Float} threshold
* @returns {Bool}
*/
_isNodeHasConnectorToDirection: function(linkedObjCoords, direction, threshold)
{
if (Kekule.ObjUtils.isUnset(threshold))
threshold = 1/5;
for (var i = 0, l = linkedObjCoords.length; i < l; ++i)
{
var coord = linkedObjCoords[i];
if ((direction === D.LTR) && (coord.x > threshold))
return true;
else if ((direction === D.RTL) && (coord.x < -threshold))
return true;
if ((direction === D.TTB) && (coord.y > threshold))
return true;
else if ((direction === D.BTT) && (coord.y < -threshold))
return true;
}
return false;
},
/** @private */
_calcConnectorVectorWeight: function(coords)
{
var totalX = 0;
var totalY = 0;
/** @ignore */
var weightValues = function(x, y)
{
var maxValue = 100;
{
if (y == 0)
return maxValue;
else
return Math.min(maxValue, Math.pow(x / y, 2));
}
};
var fixedWeightX = 1;
var fixedWeightY = 1;
for (var i = 0, l = coords.length; i < l; ++i)
{
totalX += coords[i].x * weightValues(coords[i].x, coords[i].y) * fixedWeightX;
totalY += coords[i].y * weightValues(coords[i].y, coords[i].x) * fixedWeightY;
}
return {'x': totalX, 'y': totalY};
},
/** @private */
_calcConnectorVectorOfNode: function(context, node)
{
var result = {'x': 0, 'y': 0};
var linkedObjCoords = this._getStandardizedLinkedObjRelCoords(context, node, null);
for (var i = 0, l = linkedObjCoords.length; i < l; ++i)
{
result.x += linkedObjCoords[i].x;
result.y += linkedObjCoords[i].y;
}
if (result.x === result.y === 0)
result = {'x': 1, 'y': 0};
return result;
},
/** @private */
_calcVectorAxisDirection: function(vector)
{
var absX = Math.abs(vector.x);
var absY = Math.abs(vector.y);
if (absX >= absY)
return (vector.x > 0)? D.LTR: D.RTL;
else
return (vector.y > 0)? D.TTB: D.BTT;
},
/**
* Get the automatic direction of node label. This direction should avoid bonds as well as possible.
* @param {Object} node
* @returns {Int} Value from {@link Kekule.Render.TextDirection}.
* @private
*/
_decideNodeLabelCharDirection: function(context, node)
{
var linkedObjCoords = this._getStandardizedLinkedObjRelCoords(context, node, null);
var D = Kekule.Render.TextDirection;
var directions = [D.LTR, D.RTL, D.TTB, D.BTT];
var result;
for (var i = 0, l = directions.length; i < l; ++i)
{
if (!this._isNodeHasConnectorToDirection(linkedObjCoords, directions[i]))
return directions[i];
}
// all direction has connectors, further calculate
var connectorVectorWeight = this._calcConnectorVectorWeight(linkedObjCoords);
if (Math.abs(connectorVectorWeight.x) > Math.abs(connectorVectorWeight.y))
{
if (connectorVectorWeight.x <= 0)
result = D.LTR;
else
result = D.RTL;
}
else
{
if (connectorVectorWeight.y <= 0)
result = D.TTB;
else
result = D.BTT;
}
return result;
},
/**
* Returns an array of connector angles ( to X-axis ) of node.
* @param {Object} node
* @returns {Array}
* @private
*/
_calcConnectorAnglesOfNode: function(context, node)
{
var result = [];
var linkedObjCoords = this._getStandardizedLinkedObjRelCoords(context, node, null);
for (var i = 0, l = linkedObjCoords.length; i < l; ++i)
{
var c = linkedObjCoords[i];
var angle = Math.atan2(c.y, c.x);
if (angle < 0)
angle = Math.PI * 2 + angle;
result.push(angle);
}
result.sort();
return result;
},
/**
* Get the emptiest direction around node. Returns angle of that direction.
* @param {Object} node
* @returns {Float}
* @private
*/
_getMostEmptyDirectionAngleOfNode: function(context, node)
{
/*
var allowCoordBorrow = this.getRenderCache(context).options.transformParams.allowCoordBorrow;
var angle = Kekule.ChemStructureUtils.getMostEmptyDirection2DAngleOfObj(node, null, allowCoordBorrow, true, false);
var y = Math.tan(angle);
var x = Math.sqrt(1 - y * y);
var vector = {'x': x, 'y': y};
var transformedVector;
var ctab = this.getChemObj();
var transformMatrix = this.getExtraProp2(context, ctab, this.TRANSFORM_MATRIX_FIELD);
var childTransformMatrix = this.getExtraProp2(context, ctab, this.CHILD_TRANSFORM_MATRIX_FIELD);
if (ctab && (ctab.hasNode(node, false) || ctab.hasConnector(node, false))) // is direct child of ctab
{
transformedVector = Kekule.CoordUtils.transform2DByMatrix(vector, transformMatrix);
}
else // is nested child
transformedVector = Kekule.CoordUtils.transform2DByMatrix(vector, childTransformMatrix);
var result = Math.atan(transformedVector.y, transformedVector.x);
//console.log('angle method1', Math.round(angle * 180 / Math.PI), Math.round(result * 180 / Math.PI));
return result;
*/
var result;
var angles = this._calcConnectorAnglesOfNode(context, node);
var l = angles.length;
if (l === 0)
result = 0;
else if (l === 1) // only one connector
result = -angles[0];
else // more than two connectors
{
var max = 0;
var index = 0;
for (var i = 0; i < l; ++i)
{
var a1 = angles[i];
var a2 = angles[(i + 1) % l];
var delta = a2 - a1;
if (delta < 0)
delta += Math.PI * 2;
if (delta > max)
{
max = delta;
index = i;
}
}
result = angles[index] + max / 2;
}
//console.log('angle method2', Math.round(result * 180 / Math.PI));
return result;
},
/**
* Adjust marker pos, put at the emptiest direction of obj.
* @private
*/
doAutoAdjustAttachedMarkerPos: function(obj, marker, markerMargin, allowCoordBorrow, avoidDirectionAngles)
{
if (marker && !marker.getCoordOfMode(this.getCoordMode())) // has no 2D coord, do auto position
{
obj.autoSetMarker2DPos(marker, markerMargin, allowCoordBorrow, avoidDirectionAngles);
}
},
/**
* Adjust charge or radical pos if needed.
* @private
*/
doAdjustChemPropMarkerPos: function(obj, markMargin, allowCoordBorrow, avoidDirectionAngles)
{
obj.beginUpdate();
try
{
// charge
var chargeMarker = obj.getMarkerOfType(Kekule.ChemMarker.Charge);
this.doAutoAdjustAttachedMarkerPos(obj, chargeMarker, markMargin, allowCoordBorrow, avoidDirectionAngles);
// radical
var radicalMarker = obj.getMarkerOfType(Kekule.ChemMarker.Radical);
this.doAutoAdjustAttachedMarkerPos(obj, radicalMarker, markMargin, allowCoordBorrow, avoidDirectionAngles);
// the rest
var markers = obj.getUnplacedMarkers(this.getCoordMode());
for (var i = 0, l = markers.length; i < l; ++i)
{
this.doAutoAdjustAttachedMarkerPos(obj, markers[i], markMargin, allowCoordBorrow, avoidDirectionAngles);
}
}
finally
{
obj.endUpdate();
}
},
/**
* Draw charge mark (such as +, 2-) and radical mark (./..) on node, especially on C atom in skeletal formula.
* @private
*/
doDrawElectronStateMark: function(context, group, node, markType, partialChargeDecimalsLength, distinguishSingletAndTripletRadical, markFontFamily, markFontSize, markMargin, circleStrokeWidth, color, opacity, zoom)
{
var charge = node.getCharge();
var radical = node.getRadical();
var slabel = '';
/*
if (!charge)
return null;
*/
/*
var markType = options.chargeMarkType;
var markSize = options.chargeMarkSize * options.unitLength;
*/
if ((!partialChargeDecimalsLength) || (Math.abs(charge) > Math.pow(10, -partialChargeDecimalsLength) / 2))
{
var widthCircleBorder = markType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND;
// decide mark text
// charge
var isCircled = (markType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND);
if (charge === 1)
slabel = isCircled? '\u2295': '+';
else if (charge === -1)
slabel = isCircled? '\u2296': '\u2212'; //'-';
else
{
var sCharge = partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(Math.abs(charge), partialChargeDecimalsLength): Math.abs(charge).toString();
slabel = sCharge + ((charge > 0)? '+': '\u2212' /*'-'*/);
}
}
// radical
if (radical)
{
//slabel += (radical === Kekule.RadicalOrder.DOUBLET)? '•': '••';
slabel += Kekule.Render.ChemDisplayTextUtils.getRadicalDisplayText(radical, distinguishSingletAndTripletRadical);
}
if (!slabel)
return null;
var coord = this.getTransformedCoord2D(context, node);
//console.log(slabel);
// decide direction
/*
var vector = this._calcConnectorVectorOfNode(context, node);
//console.log(vector);
vector.x = -vector.x; vector.y = -vector.y;
var vectorLength = Math.sqrt(Math.sqr(vector.x) + Math.sqr(vector.y));
*/
var dirAngle = this._getMostEmptyDirectionAngleOfNode(context, node);
var vector = {'x': Math.cos(dirAngle), 'y': Math.sin(dirAngle)};
var vectorLength = 1;
// decide position
//var markVectorLength = markMargin /* + markFontSize * chargeText.length*/;
var ratio = markMargin / vectorLength;
var markCoord = {'x': vector.x * ratio , 'y': vector.y * ratio};
//console.log(markCoord);
//alert(chargeText);
markCoord = Kekule.CoordUtils.add(coord, markCoord);
//markCoord = coord;
var BXA = Kekule.Render.BoxXAlignment;
var BYA = Kekule.Render.BoxYAlignment;
var tboxXAlign, tboxYAlign;
tboxXAlign = BXA.CENTER; tboxYAlign = BYA.CENTER;
/*
var textDirection = this._calcVectorAxisDirection(vector);
switch (textDirection)
{
case D.LTR: tboxXAlign = BXA.LEFT; tboxYAlign = BYA.CENTER; break;
case D.RTL: tboxXAlign = BXA.RIGHT; tboxYAlign = BYA.CENTER; break;
case D.TTB: tboxXAlign = BXA.CENTER; tboxYAlign = BYA.TOP; break;
case D.BTT: tboxXAlign = BXA.CENTER; tboxYAlign = BYA.BOTTOM; break;
}
*/
var elem = this.drawText(context, markCoord, slabel, {
'fontFamily': markFontFamily,
'fontSize': markFontSize,
'color': color,
'opacity': opacity,
'textBoxXAlignment': tboxXAlign,
'textBoxYAlignment': tboxYAlign,
'zoom': zoom || 1
});
/*
//alert(textDirection);
/*
var chargeGroup, drawCircle;
// draw text and circle
if (((charge === 1) || (charge === -1)) &&
(markType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND))
{
chargeGroup = this.doCreateGroup(context);
drawCircle = true;
}
if (chargeGroup)
{
this.doAddToGroup(elem, chargeGroup);
if (drawCircle)
{
var circleRadius = markFontSize / 2;
var ratio = circleRadius / vectorLength;
var baseCoord = {'x': vector.x * ratio , 'y': vector.y * ratio};
var baseCoord = Kekule.CoordUtils.add(coord, baseCoord);
elem = this.doDrawCircle(context, baseCoord, circleRadius,
circleStrokeWidth, color);
this.addToGroup(elem, chargeGroup);
}
return chargeGroup;
}
else
*/
return elem;
},
/**
* Draw a connector (bond) connecting nodes with a specified shape on context.
* @param {Object} context
* @param {Kekule.ChemStructureConnector} connector
* @param {Array} nodes
* @param {Hash} renderOptions
* @param {Hash} finalTransformOptions
* @private
*/
doDrawConnectorShape: function(context, connector, nodes, renderOptions, finalTransformOptions)
{
var CU = Kekule.CoordUtils;
//var globalOptions = this.getRenderConfigs().getLengthConfigs().toHash();
//globalOptions.color = this.getRenderConfigs().getColorConfigs().getConnectorColor();
/*
var globalOptions = this.getRenderCache(context).connectorDrawOptions;
var renderOptions = Object.create(globalOptions);
var localRenderOptions = connector.getRenderOptions();
var localOptions = Kekule.Render.RenderOptionUtils.getConnectorDrawParams(localRenderOptions);
if (localOptions)
renderOptions = Object.extend(renderOptions, localOptions);
*/
var node1 = nodes[0];
var node2 = nodes[1];
var c1 = this.getTransformedCoord2D(context, node1, finalTransformOptions.allowCoordBorrow);
var c2 = this.getTransformedCoord2D(context, node2, finalTransformOptions.allowCoordBorrow);
/*
var coord1 = Object.extend({}, this.getTransformedCoord2D(context, node1, finalTransformOptions.allowCoordBorrow));
var coord2 = Object.extend({}, this.getTransformedCoord2D(context, node2, finalTransformOptions.allowCoordBorrow));
*/
var coord1 = {'x': c1.x, 'y': c1.y};
var coord2 = {'x': c2.x, 'y': c2.y};
var nodes = [node1, node2];
var coords = [coord1, coord2];
var originDistance = CU.getDistance(coord1, coord2);
var shrinkedDistance = 0;
var unitLength = renderOptions.unitLength || 1;
var lineLength = Math.sqrt(Math.sqr(coord2.x - coord1.x) + Math.sqr(coord2.y - coord1.y));
var lineAdjustCoordDeltas = [];
// line length scale ratio
var doLineLengthScale = false, lineLengthScaleBaseNodeIndex;
var lineLengthScaleRatio = renderOptions.bondLengthScaleRatio;
if (Kekule.ObjUtils.notUnset(lineLengthScaleRatio))
{
var lineVector = CU.substract(coord2, coord1);
doLineLengthScale = true;
lineLengthScaleBaseNodeIndex = (lineLengthScaleRatio > 0)? 0: 1;
var shrinkRatio = 1 - (Math.abs(lineLengthScaleRatio || 1));
var sign = (lineLengthScaleBaseNodeIndex === 1)? -1: 1;
//var sign = 1;
lineAdjustCoordDeltas[lineLengthScaleBaseNodeIndex] = CU.multiply(lineVector, sign * shrinkRatio);
}
// node label eclipse line
if (this.getObjNeedDrawLabel(context, node1) || this.getObjNeedDrawLabel(context, node2)) // the connector should avoid overlap with node label
{
//var expandRatio = this.getRenderConfigs().getLengthConfigs().getAtomLabelBoxExpandRatio();
var expandRatio = renderOptions.atomLabelBoxExpandRatio;
var halfLabelBoxWidth, halfLabelBoxHeight;
var crossOnVerticalEdge;
// line projection on X/Y axis
var lineXPrj = coord2.x - coord1.x;
var lineYPrj = coord2.y - coord1.y;
var xSign = (lineXPrj >= 0)? 1: -1;
var ySign = (lineYPrj >= 0)? 1: -1;
var isVerticalLine = (lineXPrj === 0);
if (!isVerticalLine)
var angleTg = Math.abs(lineYPrj / lineXPrj);
if (Math.abs(lineXPrj) >= Math.abs(lineYPrj)) // bond line cross with label box in vertical edge
crossOnVerticalEdge = true;
else // cross in horizontal edge
crossOnVerticalEdge = false;
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var coord = coords[i];
var coordDelta = null;
if (this.getObjNeedDrawLabel(context, node))
{
var fSize = this._getNodeFontSize(context, node) * (finalTransformOptions.zoom || 1);
halfLabelBoxWidth = halfLabelBoxHeight = fSize * unitLength * expandRatio / 2;
if (crossOnVerticalEdge)
{
/*
coord.x += halfLabelBoxWidth * xSign;
coord.y += halfLabelBoxWidth * angleTg * ySign;
*/
coordDelta = {'x': halfLabelBoxWidth * xSign, 'y': halfLabelBoxWidth * angleTg * ySign};
}
else
{
if (!isVerticalLine)
{
/*
coord.y += halfLabelBoxHeight * ySign;
coord.x += halfLabelBoxHeight / angleTg * xSign;
*/
coordDelta = {'x': halfLabelBoxHeight / angleTg * xSign, 'y': halfLabelBoxHeight * ySign};
}
else
{
/*
coord.y += halfLabelBoxHeight * ySign;
*/
coordDelta = {'x': 0, 'y': halfLabelBoxHeight * ySign};
}
}
if (coordDelta)
{
// compare with scale delta
var scaleCoordDelta = lineAdjustCoordDeltas[i];
if (scaleCoordDelta && (Math.abs(lineLengthScaleRatio) <= 1)) // if scale ratio > 1, the line will exceed atom label and must be cut
{
if (Math.abs(coordDelta.x) > Math.abs(scaleCoordDelta.x) || Math.abs(coordDelta.y) > Math.abs(scaleCoordDelta.y))
lineAdjustCoordDeltas[i] = coordDelta;
}
else
lineAdjustCoordDeltas[i] = coordDelta;
/*
shrinkedDistance += CU.getDistance(coordDelta, {'x': 0, 'y': 0});
var newCoord = CU.add(coord, coordDelta);
coord.x = newCoord.x;
coord.y = newCoord.y;
*/
}
}
xSign *= -1;
ySign *= -1;
}
}
//console.log(coords, lineAdjustCoordDeltas, lineLengthScaleBaseNodeIndex);
for (var i = 0, l = coords.length; i < l; ++i)
{
var coord = coords[i];
var coordDelta = lineAdjustCoordDeltas[i];
if (coordDelta)
{
shrinkedDistance += CU.getDistance(coordDelta, {'x': 0, 'y': 0});
var newCoord = CU.add(coord, coordDelta);
coord.x = newCoord.x;
coord.y = newCoord.y;
}
}
if (shrinkedDistance >= originDistance) // label box too huge, can not draw connector shape
{
return null;
}
// calculate stroke / color and other styles
var strokeWidth = (renderOptions.bondLineWidth || renderOptions.strokeWidth || 1) * (renderOptions.unitLength || 1);
/*
if (lineParams[0].isBold)
strokeWidth *= renderOptions.boldBondLineWidthRatio;
*/
renderOptions.strokeWidth = strokeWidth;
/*
// temp
//var elem = this.doDrawConnectorLine(context, coord1, coord2, 1, style);
var elem = this.doDrawSymmetryLineConnector(context, node1, node2, coord1, coord2, lineLength, 3, false, true, localRenderOptions);
return elem;
*/
// firstly decide draw type of connector
var renderType = renderOptions.renderType; // Kekule.Render.RenderOptionUtils.getConnectorRenderType(localRenderOptions);
var allowedTypes = Kekule.Render.ConnectorDrawUtils.getPossibleConnectorRenderTypes(connector);
if (Kekule.ObjUtils.isUnset(renderType) || (allowedTypes.indexOf(renderType) < 0)) // render type in options is not available
renderType = allowedTypes[0]; // use default type instead
// different renderType may need different methods to draw
var result;
if (this.isLineBasedConnector(renderType))
{
result = this.doDrawLineBasedConnector(context, renderType, node1, node2,
coord1, coord2, lineLength, renderOptions);
}
else if (this.isTriangleBasedConnector(renderType))
{
result = this.doDrawTriangleBasedConnector(context, renderType, node1, node2,
coord1, coord2, lineLength, renderOptions);
}
else if (this.isTriangleHashBasedConnector(renderType))
{
result = this.doDrawTriangleHashBasedConnector(context, renderType, node1, node2,
coord1, coord2, lineLength, renderOptions);
}
else if (this.isRectangleBasedConnector(renderType))
{
result = this.doDrawRectangleBasedConnector(context, renderType, node1, node2,
coord1, coord2, lineLength, renderOptions);
}
else if (this.isWavyBasedConnector(renderType))
{
result = this.doDrawWavyBasedConnector(context, renderType, node1, node2,
coord1, coord2, lineLength, renderOptions);
}
return result;
},
/** @private */
_lineBasedConnectorTypes: [RT.SINGLE, RT.DOUBLE, RT.TRIPLE, RT.QUAD,
RT.SCISSORS_DOUBLE,
RT.BOLD, RT.BOLD_DOUBLE, RT.BOLD_TRIPLE,
RT.DASHED, RT.DASHED_DOUBLE, RT.DASHED_TRIPLE, RT.SOLID_DASH, RT.BOLD_DASH,
RT.ARROWED],
_triangleBasedConnectorTypes: [RT.WEDGED_SOLID, RT.WEDGED_SOLID_INV,
RT.WEDGED_HOLLOW, RT.WEDGED_HOLLOW_INV],
_triangleHashBasedConnectorTypes: [RT.WEDGED_HASHED, RT.WEDGED_HASHED_INV, RT.HASHED],
_rectangleBasedConnectorTypes: [RT.WEDGED_SOLID_BOTH, RT.WEDGED_HOLLOW_BOTH],
/**
* Check if a connector's render type is based on line and can be drawn by doDrawLineBasedConnector.
* @param {Int} renderType
* @returns {Bool}
* @private
*/
isLineBasedConnector: function(renderType)
{
return (this._lineBasedConnectorTypes.indexOf(renderType) >= 0);
},
/**
* Check if a connector's render type is based on triangle and can be drawn by doDrawTriangleBasedConnector.
* @param {Int} renderType
* @returns {Bool}
* @private
*/
isTriangleBasedConnector: function(renderType)
{
return (this._triangleBasedConnectorTypes.indexOf(renderType) >= 0);
},
/**
* Check if a connector's render type is based on triangle hash and can be drawn by doDrawTrangleHashBasedConnector.
* @param {Int} renderType
* @returns {Bool}
* @private
*/
isTriangleHashBasedConnector: function(renderType)
{
return (this._triangleHashBasedConnectorTypes.indexOf(renderType) >= 0);
},
/**
* Check if a connector's render type is based on rectangle and can be drawn by doDrawRectangleBasedConnector.
* @param {Int} renderType
* @returns {Bool}
* @private
*/
isRectangleBasedConnector: function(renderType)
{
return (this._rectangleBasedConnectorTypes.indexOf(renderType) >= 0);
},
/**
* Check if a connector's render type is based on wavy line and can be drawn by doDrawWavyBasedConnector.
* @param {Int} renderType
* @returns {Bool}
* @private
*/
isWavyBasedConnector: function(renderType)
{
return renderType = RT.WAVY;
},
/**
* Draw line shape connectors.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @param {Object} coord1
* @param {Object} coord2
* @param {Float} lineLength
* @param {Object} options
* @returns {Object}
* @private
*/
doDrawLineBasedConnector: function(context, renderType, node1, node2, coord1, coord2, lineLength, options)
{
var lineParams = [];
var param = {'isBold': false, 'isDash': false};
var boldParam = {'isBold': true, 'isDash': false};
var dashParam = {'isBold': false, 'isDash': true};
switch (renderType)
{
case RT.DOUBLE:
case RT.SCISSORS_DOUBLE: for (var i = 0; i < 2; ++i, lineParams.push(param)); break;
case RT.TRIPLE: for (var i = 0; i < 3; ++i, lineParams.push(param)); break;
case RT.QUAD: for (var i = 0; i < 4; ++i, lineParams.push(param)); break;
case RT.BOLD: lineParams.push(boldParam); break;
case RT.BOLD_DOUBLE: lineParams.push(boldParam); lineParams.push(param); break;
case RT.BOLD_TRIPLE: lineParams.push(boldParam); for (var i = 0; i < 2; ++i, lineParams.push(param)); break;
case RT.BOLD_QUAD: lineParams.push(boldParam); for (var i = 0; i < 3; ++i, lineParams.push(param)); break;
case RT.DASHED: lineParams.push(dashParam); break;
case RT.DASHED_DOUBLE: for (var i = 0; i < 2; ++i, lineParams.push(dashParam)); break;
case RT.DASHED_TRIPLE: for (var i = 0; i < 3; ++i, lineParams.push(dashParam)); break;
// TODO: which side is dash need to be further calculated for aromatic bond
case RT.SOLID_DASH: lineParams.push(param); lineParams.push(dashParam); break;
case RT.BOLD_DASH: lineParams.push(boldParam); lineParams.push(dashParam); break;
case RT.ARROWED: param.isArrow = true; lineParams.push(param); break;
case RT.SINGLE:
default:
lineParams.push(param);
}
var isCross = renderType === RT.SCISSORS_DOUBLE;
return this.doDrawSymmetryLineConnector(context, node1, node2, coord1, coord2, lineLength, lineParams, isCross, options);
},
/** @private */
doDrawSymmetryLineConnector: function(context, node1, node2, coord1, coord2, lineLength, lineParams, isCross, options)
{
var lineCount = lineParams.length;
var group = null, line = null;
if (lineCount === 1) // only one line
{
// draw first center line
var strokeWidth = options.strokeWidth;
if (lineParams[0].isBold)
strokeWidth *= options.boldBondLineWidthRatio;
var arrowParams = null;
if (lineParams[0].isArrow)
{
arrowParams = {'width': options.bondArrowWidth * options.unitLength, 'length': options.bondArrowLength * options.unitLength};
}
var ops = {
'strokeWidth': strokeWidth,
'strokeColor': Kekule.Render.RenderOptionUtils.getColor(options),
'strokeDash': lineParams[0].isDash,
'opacity': options.opacity
};
//console.log('draw line options', ops);
var line = this.drawArrowLine(context, coord1, coord2, arrowParams, ops);
var boundInfo = this.createLineBoundInfo(coord1, coord2, strokeWidth);
}
else if (lineCount > 1) // multiple lines, create group
{
group = this.createDrawGroup(context);
var lineGap = options.multipleBondSpacingAbs?
options.multipleBondSpacingAbs:
options.multipleBondSpacingRatio * lineLength;
if (options.multipleBondMaxAbsSpacing)
lineGap = Math.min(lineGap, options.multipleBondMaxAbsSpacing);
lineGap *= options.unitLength;
var w = coord2.x - coord1.x;
var h = coord2.y - coord1.y;
var actualLineLength = Math.sqrt(Math.sqr(w) + Math.sqr(h));
var angleSin = h / actualLineLength;
var angleCos = w / actualLineLength;
var lastLineWidth = 0;
//var adjusts = [0, 1, -1];
// calculate line offset adjustment
// positive value means to right side of curr direction, negative value means left side
var adjusts = [];
var initialBondAlign = 1;
if (!(lineCount & 1)) // line count is even, must decide which direction should put one more lines
{
if (isCross) // cross multiple bonds, always align to center
initialBondAlign = 0;
else
initialBondAlign = Kekule.ObjUtils.isUnset(lineParams.bondAlign)?
this._decideEvenBondAlign(context, node1, node2):
lineParams.bondAlign;
//console.log(initialBondAlign);
}
for (var i = 0; i < lineCount; ++i)
{
if (Kekule.ObjUtils.notUnset(lineParams[i].offsetAdjust))
adjusts.push(lineParams[i].offsetAdjust);
else // calculate automatically
{
if ((i === 0) && (initialBondAlign !== 0))
adjusts.push(0);
else
{
var sign = (i & 1) ? 1 : -1; //(i mod 2)
//if (lineCount & 1) // line count is odd, in other word, a set of symmetric lines
if (initialBondAlign !== 0) // not a symmetry double bond
{
var offsetAdjust = initialBondAlign * sign * ((i + 1) >> 1);
}
else // symmetry double bond and so on
{
var offsetAdjust = ((i >> 1) * 1 + 0.5) * sign;
}
adjusts.push(offsetAdjust);
}
}
}
var currGap = lineGap;
if (currGap > maxGap)
maxGap = currGap;
if (currGap < minGap)
minGap = currGap;
var averCoord1 = {'x': 0, 'y': 0};
var averCoord2 = {'x': 0, 'y': 0};
var maxGap = 0, minGap = 0;
var realDrawParams = [];
for (var i = 0; i < lineCount; ++i)
{
var localOptions = Object.create(options);
var deltaX = currGap * angleSin;
var deltaY = currGap * angleCos;
var newCoord1 = {
'x': coord1.x - deltaX * adjusts[i],
'y': coord1.y + deltaY * adjusts[i]
};
var newCoord2 = {
'x': coord2.x - deltaX * adjusts[i],
'y': coord2.y + deltaY * adjusts[i]
};
averCoord1 = Kekule.CoordUtils.add(averCoord1, newCoord1);
averCoord2 = Kekule.CoordUtils.add(averCoord2, newCoord2);
var arrowParams = null;
if (lineParams[i].isArrow)
{
arrowParams = {
'width': options.bondArrowWidth * options.unitLength,
'length': options.bondArrowLength * options.unitLength
};
}
var strokeWidth = options.strokeWidth;
if (lineParams[i].isBold)
strokeWidth *= options.boldBondLineWidthRatio;
localOptions.strokeColor = options.color;
localOptions.strokeDash = lineParams[i].isDash;
/*
var line = this.drawArrowLine(context, newCoord1, newCoord2, arrowParams, localOptions);
this.addToDrawGroup(line, group);
*/
realDrawParams.push({
'coord1': newCoord1,
'coord2': newCoord2,
'arrowParams': arrowParams,
'drawOptions': localOptions
});
if (lineParams[i].isBold)
currGap = lineGap + Math.floor(strokeWidth / 2);
else
currGap = lineGap;
}
// do real draw
if (isCross)
{
//var middleLineIndex = (lineCount % 1)? (lineCount >> 1) + 1: null;
for (var i = 0; i < lineCount; ++i)
{
var currPartParam = realDrawParams[i];
var counterPartParam = realDrawParams[lineCount - i - 1];
var line = this.drawArrowLine(context, currPartParam.coord1, counterPartParam.coord2, currPartParam.arrowParams, currPartParam.drawOptions);
this.addToDrawGroup(line, group);
}
}
else
{
for (var i = 0; i < lineCount; ++i)
{
var realParam = realDrawParams[i];
var line = this.drawArrowLine(context, realParam.coord1, realParam.coord2, realParam.arrowParams, realParam.drawOptions);
this.addToDrawGroup(line, group);
}
}
averCoord1 = Kekule.CoordUtils.divide(averCoord1, lineCount);
averCoord2 = Kekule.CoordUtils.divide(averCoord2, lineCount);
var boundInfo = this.createLineBoundInfo(averCoord1, averCoord2, maxGap - minGap);
}
var result = {'element': group || line, 'boundInfo': boundInfo};
return result;
},
/** @private */
_getStandardizedLinkedObjRelCoords: function(context, node, excludeNode)
{
var result = [];
var baseCoord = this.getTransformedCoord2D(context, node);
//var linkedObjs = node.getLinkedObjs();
var linkedObjs = node.getLinkedExposedObjs();
for (var i = 0, l = linkedObjs.length; i < l; ++i)
{
var obj = linkedObjs[i];
if (obj === excludeNode)
continue;
var coord = this.getTransformedCoord2D(context, obj);
var newCoord = Kekule.CoordUtils.substract(coord, baseCoord);
/*
var len = Math.sqrt(Math.sqr(newCoord.x) + Math.sqr(newCoord.y));
newCoord = {'x': newCoord.x / len, 'y': newCoord.y / len}
*/
newCoord = Kekule.CoordUtils.standardize(newCoord);
result.push(newCoord);
}
return result;
},
/**
* Calculate how to align a double bond automatically.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @returns {Int} 1 (align to x/y increasing direction) or -1 (align to x/y decreasing direction) or 0 (align center).
*/
_decideEvenBondAlign: function(context, node1, node2)
{
var bondVector = Kekule.CoordUtils.substract(this.getTransformedCoord2D(context, node2),
this.getTransformedCoord2D(context, node1));
bondVector = Kekule.CoordUtils.standardize(bondVector);
//var sign = -Math.sign(bondBox.x) * Math.sign(bondBox.y);
// indicate this is a rather steep or gentle line
//var sign = (Math.abs(bondBox.y) > Math.abs(bondBox.x))? -1: 1;
var coords1 = this._getStandardizedLinkedObjRelCoords(context, node1, node2);
var coords2 = this._getStandardizedLinkedObjRelCoords(context, node2, node1);
var sumAlign = 0;
var sumVector = {'x': 0, 'y': 0};
for (var i = 0, l = coords1.length; i < l; ++i)
{
sumAlign += this._calcSinglePointAlign(coords1[i], bondVector);
sumVector.x += coords1[i].x;
sumVector.y += coords1[i].y;
}
for (var i = 0, l = coords2.length; i < l; ++i)
{
sumAlign += this._calcSinglePointAlign(coords2[i], bondVector);
sumVector.x += coords2[i].x;
sumVector.y += coords2[i].y;
}
if (sumAlign !== 0)
return Math.sign(sumAlign);
else
return this._calcSinglePointAlign(sumVector, bondVector);
/*
var l = Math.sqrt(Math.sqr(sumX) + Math.sqr(sumY));
sumX /= l;
sumY /= l;
//console.log(sumX, sumY, l);
l = Math.sqrt(Math.sqr(bondBox.x) + Math.sqr(bondBox.y));
bondBox.x /= l;
bondBox.y /= l;
var absSumX = Math.abs(sumX) - Math.abs(bondBox.x);
var absSumY = Math.abs(sumY) - Math.abs(bondBox.y);
//console.log(sumX, sumY, bondBox);
var delta = absSumX - absSumY;
console.log(delta);
var thredhold = 1e-3;
if (delta > thredhold)
{
return ((sumX >= 0)? 1: -1) * (-Math.sign(bondBox.y));
}
else if (delta < -thredhold)
{
return ((sumY >= 0)? 1: -1) * (Math.sign(bondBox.x));
}
else // equal
return 0;
*/
},
/** @private */
_calcSinglePointAlign: function(coord, baseVector)
{
var result;
var thredhold = 1e-4;
if (Math.abs(baseVector.x) < thredhold) // base vector is vertical line
{
if (Math.abs(coord.x) < thredhold)
result = 0;
else
result = -Math.sign(coord.x) * Math.sign(baseVector.y);
return result;
}
else
{
var y = baseVector.y * coord.x / baseVector.x;
var delta = coord.y - y;
if (delta > thredhold)
result = 1
else if (delta < -thredhold)
result = -1
else
result = 0;
result *= Math.sign(baseVector.x);
return result;
}
},
/**
* Draw triangle shape connectors.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @param {Object} coord1
* @param {Object} coord2
* @param {Float} lineLength
* @param {Object} options
* @returns {Object}
* @private
*/
doDrawTriangleBasedConnector: function(context, renderType, node1, node2, coord1, coord2, lineLength, options)
{
var isFilled = (renderType === RT.WEDGED_SOLID) || (renderType === RT.WEDGED_SOLID_INV);
var isInverted = (renderType === RT.WEDGED_SOLID_INV) || (renderType === RT.WEDGED_HOLLOW_INV);
var baseCoords = isInverted? [coord2, coord1]: [coord1, coord2];
var w = options.bondWedgeWidth * options.unitLength;
/*
var bondBox = Kekule.CoordUtils.substract(baseCoords[1], baseCoords[0]);
var len = Math.sqrt(Math.sqr(bondBox.x) + Math.sqr(bondBox.y));
var angleSin = bondBox.y / len;
var angleCos = bondBox.x / len;
var coords = [baseCoords[0]]; // start point of triangle
coords.push({'x': baseCoords[1].x + w * angleSin, 'y': baseCoords[1].y - w * angleCos});
coords.push({'x': baseCoords[1].x - w * angleSin, 'y': baseCoords[1].y + w * angleCos});
*/
var coords = this._calcTriangleBaselineCoords(baseCoords[0], baseCoords[1], w);
var localOptions = Object.create(options);
localOptions.strokeColor = options.color;
localOptions.fillColor = isFilled? options.color: null;
//var result = this.doDrawTiangle(context, coords[0], coords[1], coords[2], options.strokeWidth);
var elem = this.drawTriangle(context, baseCoords[0], coords[0], coords[1],
/*
{
'strokeWidth': options.strokeWidth,
'strokeColor': options.color,
'fillColor': isFilled? options.color: null,
'opacity': options.opacity
}*/localOptions);
var result = {'element': elem};
result.boundInfo = this.createLineBoundInfo(baseCoords[0], baseCoords[1], w);
return result;
},
/**
* Draw triangle hash shape connectors.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @param {Object} coord1
* @param {Object} coord2
* @param {Float} lineLength
* @param {Object} options
* @returns {Object}
* @private
*/
doDrawTriangleHashBasedConnector: function(context, renderType, node1, node2, coord1, coord2, lineLength, options)
{
var isInverted = (renderType === RT.WEDGED_HASHED_INV);
var isTriangle = (renderType !== RT.HASHED);
var baseCoords = isInverted? [coord2, coord1]: [coord1, coord2];
var gap = options.hashSpacing * options.unitLength;
var maxWidth = options.bondWedgeWidth * options.unitLength;
var minWidth = (isTriangle)?
options.bondWedgeHashMinWidth * options.unitLength:
maxWidth;
var deltaW = maxWidth - minWidth;
var box = Kekule.CoordUtils.substract(baseCoords[1], baseCoords[0]);
var totalLength = Math.sqrt(Math.sqr(box.x) + Math.sqr(box.y));
var hashCount = Math.floor(totalLength / gap);
var group = this.createDrawGroup(context);
var localOptions = Object.create(options);
localOptions.strokeColor = options.color;
// draw hash lines
for (var i = 0; i < hashCount; ++i)
{
//if (i === 0) // first point, should be a point, not a line
var l = gap * i;
var ratio = l / totalLength;
var stepCoord = {'x': box.x * ratio, 'y': box.y * ratio};
stepCoord = Kekule.CoordUtils.add(stepCoord, baseCoords[0]);
var baselineCoords = this._calcTriangleBaselineCoords(baseCoords[0], stepCoord, deltaW * ratio + minWidth);
var elem = this.drawLine(context, baselineCoords[0], baselineCoords[1],
/*
{
'strokeWidth': options.strokeWidth,
'strokeColor': options.color,
'opacity': options.opacity
}*/localOptions);
this.addToDrawGroup(elem, group);
}
var result = {'element': group};
result.boundInfo = this.createLineBoundInfo(baseCoords[0], baseCoords[1], maxWidth);
return result;
},
/** @private */
_calcTriangleBaselineCoords: function(vertexCoord, midPointCoord, baseLineWidth)
{
var box = Kekule.CoordUtils.substract(midPointCoord, vertexCoord);
var len = Math.sqrt(Math.sqr(box.x) + Math.sqr(box.y));
var angleSin = box.y / len;
var angleCos = box.x / len;
var w = baseLineWidth / 2;
var result = [];
result.push({'x': midPointCoord.x + w * angleSin, 'y': midPointCoord.y - w * angleCos});
result.push({'x': midPointCoord.x - w * angleSin, 'y': midPointCoord.y + w * angleCos});
return result;
},
/**
* Draw rectangle shape connectors.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @param {Object} coord1
* @param {Object} coord2
* @param {Float} lineLength
* @param {Object} options
* @returns {Object}
* @private
*/
doDrawRectangleBasedConnector: function(context, renderType, node1, node2, coord1, coord2, lineLength, options)
{
//console.log('draw rectangle connector');
var isFilled = (renderType === RT.WEDGED_SOLID_BOTH);
var w = options.bondWedgeWidth * options.unitLength;
var coords = this._calcRectangleCornerCoords(coord1, coord2, w);
var localOptions = Object.create(options);
localOptions.strokeColor = options.color;
localOptions.fillColor = isFilled? options.color: null;
var path = Kekule.Render.DrawPathUtils.makePath(
'M', [coords[0].x, coords[0].y],
'L', [coords[1].x, coords[1].y],
'L', [coords[2].x, coords[2].y],
'L', [coords[3].x, coords[3].y],
'Z'
);
var elem = this.drawPath(context, path, localOptions);
var result = {'element': elem};
result.boundInfo = this.createLineBoundInfo(coord1, coord2, w);
return result;
},
/** @private */
_calcRectangleCornerCoords: function(end1Coord, end2Coord, baseLineWidth)
{
var box = Kekule.CoordUtils.substract(end2Coord, end1Coord);
var len = Math.sqrt(Math.sqr(box.x) + Math.sqr(box.y));
var angleSin = box.y / len;
var angleCos = box.x / len;
var w = baseLineWidth / 2;
var result = [];
var delta = {'x': w * angleSin, 'y': w * angleCos};
result.push({'x': end1Coord.x + delta.x, 'y': end1Coord.y - delta.y});
result.push({'x': end1Coord.x - delta.x, 'y': end1Coord.y + delta.y});
result.push({'x': end2Coord.x - delta.x, 'y': end2Coord.y + delta.y});
result.push({'x': end2Coord.x + delta.x, 'y': end2Coord.y - delta.y});
return result;
},
/**
* Draw wavy shape connectors.
* @param {Object} context
* @param {Object} node1
* @param {Object} node2
* @param {Object} coord1
* @param {Object} coord2
* @param {Float} lineLength
* @param {Object} options
* @returns {Object}
* @private
*/
doDrawWavyBasedConnector: function(context, renderType, node1, node2, coord1, coord2, lineLength, options)
{
var CU = Kekule.CoordUtils;
var refRadius = options.bondWavyRadius * options.unitLength;
/*
var dx = coord2.x - coord2.x;
var dy = coord2.y - coord1.y;
*/
var bondLength = CU.getDistance(coord1, coord2);
var arcCount = Math.round(bondLength / refRadius);
var coordDelta = CU.divide(CU.substract(coord2, coord1), arcCount);
var arcRadius = bondLength / arcCount;
var pathParams = [];
var beginCoord;
var endCoord = coord1;
var angle = Math.atan2(coordDelta.y, coordDelta.x) + Math.PI;
var controlPointDelta = {
'x': arcRadius / 1.5 * Math.sin(angle),
'y': -arcRadius / 1.5 * Math.cos(angle)
};
//var controlPointDelta = {'x': 0, 'y': 20};
var sign = 1;
for (var i = 0; i < arcCount; ++i)
{
sign = -sign
beginCoord = endCoord;
endCoord = CU.add(beginCoord, coordDelta);
var c = CU.multiply(controlPointDelta, sign);
var ctrlCoord1 = CU.add(beginCoord, c);
var ctrlCoord2 = CU.add(endCoord, c);
if (i === 0)
{
pathParams.push('M');
pathParams.push([beginCoord.x, beginCoord.y]);
}
pathParams.push('C');
pathParams.push([ctrlCoord1.x, ctrlCoord1.y, ctrlCoord2.x, ctrlCoord2.y, endCoord.x, endCoord.y]);
}
var path = Kekule.Render.DrawPathUtils.makePath.apply(Kekule.Render.DrawPathUtils, pathParams);
var localOptions = Object.create(options);
localOptions.strokeColor = options.color;
localOptions.fillColor = null;
//var result = this.doDrawTiangle(context, coords[0], coords[1], coords[2], options.strokeWidth);
var elem = this.drawPath(context, path, localOptions);
var result = {'element': elem};
result.boundInfo = this.createLineBoundInfo(coord1, coord2, arcRadius);
return result;
}
});
/**
* Class to render for {@link Kekule.StructureFragment}.
* The class will use {@link Kekule.Render.ChemCtab2DRenderer} or {@link Kekule.Render.Formula2DRenderer} to draw actual structure.
* @class
* @augments Kekule.Render.ChemObj2DRenderer
*
* @param {Kekule.ChemObject} chemObj Object to be drawn.
* @param {Object} drawBridge A object that implements the actual draw job.
* @param {Object} renderConfigs Global configuration for rendering.
* This property should be an instance of {@link Kekule.Render.Render2DConfigs}.
* //@param {Int} moleculeDisplayType Display type of molecule of current ctab.
* @param {Kekule.ObjectEx} parent Parent object of this renderer, usually another renderer or an instance of {@link Kekule.Render.ChemObjPainter}, or null.
*
* //@property {Int} moleculeDisplayType Display type of molecule.
*/
Kekule.Render.StructFragment2DRenderer = Class.create(Kekule.Render.ChemObj2DRenderer,
/** @lends Kekule.Render.StructFragment2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.StructFragment2DRenderer',
/** @constructs */
initialize: function($super, chemObj, drawBridge, /*renderConfigs,*/ parent)
{
$super(chemObj, drawBridge, /*renderConfigs,*/ parent);
this._concreteRenderer = null;
this._concreteChemObj = null;
this.initConcreteRenderer();
//this.setMoleculeDisplayType(moleculeDisplayType || Kekule.Render.MoleculeDisplayType.BOND_LINE);
},
finalize: function($super)
{
$super();
if (this._concreteRenderer)
{
this._concreteRenderer.finalize();
this._concreteRenderer = null;
}
},
/** @private */
initConcreteRenderer: function()
{
var chemObj = this.getChemObj();
var drawBridge = this.getDrawBridge();
if (this._isRendererMismatch(this._concreteRenderer, chemObj) && this._concreteRenderer)
{
this._concreteRenderer.finalize();
this._concreteRenderer = null;
this._concreteChemObj = null;
}
if (!this._concreteRenderer && chemObj)
{
if (chemObj.hasCtab())
{
this._concreteChemObj = chemObj.getCtab();
this._concreteRenderer = new Kekule.Render.ChemCtab2DRenderer(chemObj.getCtab(), drawBridge, /*renderConfigs,*/ this);
}
else if (chemObj.hasFormula())
{
this._concreteChemObj = chemObj.getFormula();
this._concreteRenderer = new Kekule.Render.Formula2DRenderer(chemObj.getFormula(), drawBridge, /*renderConfigs,*/ this);
}
}
return this._concreteRenderer;
},
/** @private */
_isRendererMismatch: function(renderer, chemObj)
{
return (renderer && !chemObj) ||
((renderer instanceof Kekule.Render.ChemCtab2DRenderer) && !chemObj.hasCtab()) ||
((renderer instanceof Kekule.Render.Formula2DRenderer) && !chemObj.hasFormula());
},
/** @private */
getConcreteRenderer: function()
{
this.initConcreteRenderer();
return this._concreteRenderer;
},
/** @private */
/*
initProperties: function()
{
this.defineProp('moleculeDisplayType',
{
'dataType': DataType.INT, 'serializable': false,
'getter': function()
{
if (this._concreteRenderer.getMoleculeDisplayType)
return this._concreteRenderer.getMoleculeDisplayType();
else
return null;
},
'setter': function(value)
{
if (this._concreteRenderer.setMoleculeDisplayType)
return this._concreteRenderer.setMoleculeDisplayType(value);
else
return null;
}
});
},
*/
/** @private */
/*
doEstimateSelfObjBox: function(context, options)
{
return this._concreteRenderer.estimateObjBox(context, options);
},
*/
/** @private */
/*
doEstimateRenderBox: function(context, baseCoord, options)
{
return this._concreteRenderer.estimateRenderBox(context, baseCoord, options);
},
*/
//* @private */
/*
applyConfigs: function()
{
this._concreteRenderer.setRenderConfigs(this.getRenderConfigs());
this._concreteRenderer.setDrawBridge(this.getDrawBridge());
},
*/
/** @private */
_getConcreteRendererDrawOptions: function(options)
{
/*
var ops = Object.create(options);
var chemObj = this.getChemObj();
var objOptions = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
chemObj.getOverriddenRender3DOptions(): chemObj.getOverriddenRenderOptions();
ops = Object.extend(ops, objOptions || {});
return ops;
*/
return options;
},
/** @ignore */
isChemObjRenderedBySelf: function($super, context, obj)
{
var r = this.getConcreteRenderer();
var result = $super(context, obj) || (obj === this.getChemObj()) || (r && r.isChemObjRenderedBySelf(context, obj));
return result;
},
/** @ignore */
isChemObjRenderedDirectlyBySelf: function($super, context, obj)
{
var r = this.getConcreteRenderer();
return $super(context, obj) || (obj === this.getChemObj()) || (obj === this._concreteChemObj); // || (r && r.isChemObjRenderedDirectlyBySelf(context, obj));
},
/** @private */
doSetRedirectContext: function($super, value)
{
$super(value);
var r = this.getConcreteRenderer();
if (r)
r.setRedirectContext(value);
},
/** @ignore */
getChildObjs: function($super)
{
var chemObj = this.getChemObj();
if (chemObj)
{
var r = [];
var childObjs = (chemObj.getExposedNodes() || []).concat(chemObj.getExposedConnectors() || []);
for (var i = 0, l = childObjs.length; i < l; ++i)
{
var obj = childObjs[i];
if (obj.getAttachedMarkers)
r = r.concat(obj.getAttachedMarkers() || []);
}
return r.concat($super());
}
else
return $super();
},
/** @ignore */
_needWholelyDraw: function($super, partialDrawObjs, context)
{
var result = $super(partialDrawObjs, context);
if (!result)
{
var chemObj = this.getChemObj();
var hasStructObjs = false;
for (var i = 0, l = partialDrawObjs.length; i < l; ++i)
{
var pObj = partialDrawObjs[i];
if (pObj.isChildOf(chemObj) /* && (pObj instanceof Kekule.ChemStructureObject)*/) // child attachers also need redraw whole
{
hasStructObjs = true;
break;
}
}
result = hasStructObjs;
}
return result;
},
/** @private */
_createChargeAndRadicalMarkerOnStructFragment: function(mol)
{
mol.beginUpdate();
try
{
if (mol.getCharge && mol.getCharge())
mol.fetchChargeMarker(true);
// then the children
var nodes = mol.getNodes();
for (var i = 0, l = mol.getNodeCount(); i < l; ++i)
{
var node = mol.getNodeAt(i);
node.beginUpdate();
try
{
if (node.getCharge())
node.fetchChargeMarker(true);
if (node.getRadical())
node.fetchRadicalMarker(true);
if (node.getNodeAt) // is sub fragment
this._createChargeAndRadicalMarkerOnStructFragment(node);
}
finally
{
node.endUpdate();
}
}
}
finally
{
//console.log('<done add marker>');
mol.endUpdate();
}
},
/** @ignore */
doDraw: function($super, context, baseCoord, options)
{
// do some initial jobs on struct fragment
var useChargeAndRadicalMarkers = !!options.autoCreateChargeAndRadicalMarker;
if (this.getCanModifyTargetObj() && useChargeAndRadicalMarkers)
{
//console.log(options.autoCreateChargeAndRadicalMarker, options);
var mol = this.getChemObj();
this._createChargeAndRadicalMarkerOnStructFragment(mol);
}
$super(context, baseCoord, options);
},
/** @ignore */
doDrawSelf: function($super, context, baseCoord, options)
{
//this.applyConfigs();
$super(context, baseCoord, options);
/*
var transformOptions = this.calcActualTransformOptions(context, this.getChemObj(), baseCoord, options);
var op = Object.create(options);
op = Object.extend(op, transformOptions);
op.baseOnRootCoord = false;
console.log(options, op);
*/
var chemObj = this.getChemObj();
var actualBaseCoord = baseCoord;
var r = this.getConcreteRenderer();
//console.log('actualBaseCoord before', actualBaseCoord, options.transformParams);
if ((!actualBaseCoord) && (r instanceof Kekule.Render.Formula2DRenderer)) // need calc center coord for formula manually
{
if (chemObj.getAbsBaseCoord2D)
{
var coord = chemObj.getAbsBaseCoord2D(options.allowCoordBorrow);
//console.log('chemObj coord', coord);
actualBaseCoord = Kekule.CoordUtils.transform2D(coord, options.transformParams);
}
//console.log('actualBaseCoord', actualBaseCoord);
}
/*
else
console.log('baseCoord set', baseCoord);
*/
if (!chemObj.hasFormula() && !chemObj.hasCtab()) // no context, need not to draw
return null;
else if (r)
{
//console.log('concrete draw', r.getClassName(), options.partialDrawObjs, !!r.getRedirectContext());
/*
var op = Object.create(options);
if (op.partialDrawObjs) // molecule is a whole and node/connectors can not be partial drawn
{
var hasStructObjs = false;
for (var i = 0, l = op.partialDrawObjs.length; i < l; ++i)
{
var pObj = op.partialDrawObjs[i];
if ((pObj.getParent() === chemObj) && (pObj instanceof Kekule.ChemStructureObject))
{
hasStructObjs = true;
break;
}
}
if (hasStructObjs)
op.partialDrawObjs = null;
}
*/
return r.draw(context, actualBaseCoord, this._getConcreteRendererDrawOptions(options));
}
},
/** @ignore */
doRedraw: function($super, context)
{
return $super(context);
/*
var r = this.getConcreteRenderer();
if (r)
return r.redraw(context);
*/
},
/** @ignore */
doUpdateSelf: function(context, updatedObjDetails, updateType)
{
var objs = this._extractObjsOfUpdateObjDetails(updatedObjDetails);
if (objs.indexOf(this.getChemObj()) >= 0) // root object need to be updated
{
var p = this.getRenderCache(context);
this.doClear(context);
return this.draw(context, p.baseCoord, p.options);
}
var r = this.getConcreteRenderer();
if (r)
return r.doUpdate(context, updatedObjDetails, updateType);
},
/** @ignore */
doClearSelf: function(context)
{
var r = this.getConcreteRenderer();
if (r)
return r.clear(context);
},
/** @ignore */
estimateRenderBox: function(context, baseCoord, options, allowCoordBorrow)
{
var r = this.getConcreteRenderer();
if (r)
return r.estimateRenderBox(context, baseCoord, this._getConcreteRendererDrawOptions(options), allowCoordBorrow);
else
return null;
},
/** @ignore */
transformCoordToObj: function($super, context, chemObj, coord)
{
return $super(context, chemObj, coord);
//console.log(chemObj, this.getChemObj(), chemObj === this.getChemObj());
/*
var obj = (this.getChemObj() === chemObj)? this._concreteChemObj: chemObj;
var r = this.getConcreteRenderer();
if (r)
return r.transformCoordToObj(context, obj, coord);
else
return coord;
*/
},
/** @ignore */
transformCoordToContext: function($super, context, chemObj, coord)
{
return $super(context, chemObj, coord);
/*
//console.log(chemObj, this.getChemObj(), chemObj === this.getChemObj());
var obj = (this.getChemObj() === chemObj)? this._concreteChemObj: chemObj;
var r = this.getConcreteRenderer();
if (r)
return r.transformCoordToContext(context, obj, coord);
else
return coord;
*/
}
});
// Molecule renderer, actually an alias of structFragment renderer
Kekule.Render.Mol2DRenderer = Kekule.Render.StructFragment2DRenderer;
/**
* Base class to render composite chem objects, such as composite molecule, chem space and so on.
* The class will use concrete renderer for each child object inside.
* @class
* @augments Kekule.Render.ChemObj2DRenderer
*
* @param {Kekule.ChemObject} chemObj Object to be drawn.
* @param {Object} drawBridge A object that implements the actual draw job.
* @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.
*/
Kekule.Render.CompositeObj2DRenderer = Class.create(Kekule.Render.ChemObj2DRenderer,
/** @lends Kekule.Render.CompositeObj2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.CompositeObj2DRenderer'
/* @constructs */
/*
initialize: function($super, chemObj, drawBridge, renderConfigs, parent)
{
$super(chemObj, drawBridge, renderConfigs, parent);
}
*/
});
//Kekule.Render.RendererDefineUtils.addCompositeRenderSupport(Kekule.Render.CompositeObj2DRenderer);
/**
* Class to render composite molecule.
* @class
* @augments Kekule.Render.CompositeObj2DRenderer
*/
Kekule.Render.CompositeMolecule2DRenderer = Class.create(Kekule.Render.CompositeObj2DRenderer,
/** @lends Kekule.Render.CompositeMolecule2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.CompositeMolecule2DRenderer',
/** @ignore */
getChildObjs: function($super)
{
var r = [];
var group = this.getChemObj().getSubMolecules();
for (var i = 0, l = group.getItemCount(); i < l; ++i)
{
var o = group.getObjAt(i);
r.push(o);
}
return r.concat($super());
}
});
/**
* Class to render ChemObjList or ChemStructureObjectGroup.
* @class
* @augments Kekule.Render.CompositeObj2DRenderer
*/
Kekule.Render.ChemObjGroupList2DRenderer = Class.create(Kekule.Render.CompositeObj2DRenderer,
/** @lends Kekule.Render.CompositeMolecule2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.CompositeMolecule2DRenderer',
/** @ignore */
getChildObjs: function($super)
{
var obj = this.getChemObj();
if (obj instanceof Kekule.ChemObjList)
{
var r = [];
for (var i = 0, l = obj.getItemCount(); i < l; ++i)
{
r.push(obj.getItemAt(i));
}
}
else if (obj instanceof Kekule.ChemStructureObjectGroup)
{
r = obj.getAllObjs();
}
return (r || []).concat($super());
}
});
/**
* Class to render reactions.
* @class
* @augments Kekule.Render.CompositeObj2DRenderer
*/
Kekule.Render.Reaction2DRenderer = Class.create(Kekule.Render.CompositeObj2DRenderer,
/** @lends Kekule.Render.Reaction2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.Reaction2DRenderer',
/** @ignore */
getChildObjs: function($super)
{
var r = [];
var reaction = this.getChemObj();
var r = [];
// reactants
for (var i = 0, l = reaction.getReactantCount(); i < l; ++i)
{
var o = reaction.getReactantAt(i);
r.push(o);
}
// products
for (var i = 0, l = reaction.getProductCount(); i < l; ++i)
{
var o = reaction.getProductAt(i);
r.push(o);
}
return (r || []).concat($super());
// TODO: currently the reagent is not considered
}
});
/**
* Class to render ChemSpaceElement instance.
* The class will use concrete renderer for each child object inside.
* @class
* @augments Kekule.Render.CompositeObj2DRenderer
*
* @param {Kekule.ChemObject} chemObj Object to be drawn.
* @param {Object} drawBridge A object that implements the actual draw job.
* @param {Object} renderConfigs Global configuration for rendering.
* This property should be an instance of {@link Kekule.Render.Render2DConfigs}.
*/
Kekule.Render.ChemSpaceElement2DRenderer = Class.create(Kekule.Render.CompositeObj2DRenderer,
/** @lends Kekule.Render.ChemSpaceElement2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ChemSpaceElement2DRenderer',
/** @ignore */
getChildObjs: function($super)
{
var result = $super() || [];
var elem = this.getChemObj();
result = (elem.getChildren().toArray() || []).concat(result);
return result;
}
});
/**
* Class to render ChemSpace instance.
* The class will use concrete renderer for each child object inside.
* @class
* @augments Kekule.Render.CompositeObj2DRenderer
*
* @param {Kekule.ChemObject} chemObj Object to be drawn.
* @param {Object} drawBridge A object that implements the actual draw job.
* @param {Object} renderConfigs Global configuration for rendering.
* This property should be an instance of {@link Kekule.Render.Render2DConfigs}.
*/
Kekule.Render.ChemSpace2DRenderer = Class.create(Kekule.Render.CompositeObj2DRenderer,
/** @lends Kekule.Render.ChemSpace2DRenderer# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ChemSpace2DRenderer',
/** @ignore */
getChildObjs: function($super)
{
//return this.getChemObj().getRoot().getChildren().toArray();
return [this.getChemObj().getRoot()].concat($super() || []);
},
/** @private */
doEstimateSelfObjBox: function($super, context, options, allowCoordBorrow)
{
var size = this.getChemObj().getSize2D();
if (size.x && size.y && options.useExplicitSpaceSize)
{
var result = {
'x1': 0,
'y1': 0,
'x2': size.x,
'y2': size.y
};
//console.log('space box', result);
return result;
}
else
return $super(context, options, allowCoordBorrow);
}
/* @private */
/*
doDrawSelf: function($super, context, baseCoord, options)
{
$super(context, baseCoord, options);
var chemObj = this.getChemObj();
var size = chemObj.getSize2D();
var transformOptions = options.transformParams;
var coord1 = Kekule.CoordUtils.transform2D({'x': 0, 'y': 0}, transformOptions);
var coord2 = Kekule.CoordUtils.transform2D({'x': size.x, 'y': size.y}, transformOptions);
var drawOptions = {
'strokeColor': '#ff0000',
'fillColor': '#00ff00',
'opacity': 0.2
};
//console.log('draw space', coord1, coord2, transformOptions);
//console.log('matrix', Kekule.CoordUtils.calcTransform2DMatrix(transformOptions));
var result = this.drawRect(context, coord1, coord2, drawOptions);
return result;
}
*/
});
// register renderers
Kekule.Render.Renderer2DFactory.register(Kekule.ChemMarker.Charge, Kekule.Render.TextBasedChemMarker2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemMarker.Radical, Kekule.Render.TextBasedChemMarker2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemMarker.UnbondedElectronSet, Kekule.Render.UnbondedElectronSetRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.TextBlock, Kekule.Render.TextBlock2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ImageBlock, Kekule.Render.ImageBlock2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.StructureFragment, Kekule.Render.StructFragment2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.CompositeMolecule, Kekule.Render.CompositeMolecule2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.Reaction, Kekule.Render.Reaction2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemObjList, Kekule.Render.ChemObjGroupList2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemStructureObjectGroup, Kekule.Render.ChemObjGroupList2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemSpaceElement, Kekule.Render.ChemSpaceElement2DRenderer);
Kekule.Render.Renderer2DFactory.register(Kekule.ChemSpace, Kekule.Render.ChemSpace2DRenderer);
})();