kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,652 lines (1,602 loc) • 103 kB
JavaScript
/**
* @fileoverview
* Utility classes and methods for rendering 2D or 3D molecule structures.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /core/kekule.structures.js
* requires /render/kekule.render.base.js
* requires /render/kekule.render.extensions.js
* requires /render/kekule.render.renderColorData.js
*/
/**
* Contains constants for rich text manipulation.
* @class
*/
Kekule.Render.RichText = {
/** Indicate an rich text item is text section. */
SECTION: 'section',
/** Indicate an rich text item is group. */
GROUP: 'group',
/** Indicate an rich text group contains multiline. */
LINES: 'lines',
/** Superscript. */
SUP: 'superscript',
/** Subscript */
SUB: 'subscript',
/** Normal text */
NORMAL: 'normal'
};
/**
* Methods to manipulate rich format text.
* A rich format text is consists of a array of objects. For example:
* {
* role: 'seq',
* anchorItem: itemRef, // or default first item, must be the direct child of group or seq
* items: [
* {
* role: 'lines', // this special role is used in multiline text
* items: [
* {
* role: 'section',
* text: 'Text1',
* //font: 'arial bold italic 10px',
* textType: 'subscript',
* refItem: anRichTextItem, // the subscript or superscript is attached to which one? If not set, regard prev one as refItem
* horizontalAlign: 1, // value from Kekule.Render.TextAlign
* verticalAlign: 1,
* charDirection: 1,
* overhang: 0.1,
* oversink: 0.1,
* _noAlignRect: true, // a special property to tell the drawer that this item should not be considered into align box.
* // super/subscript defaultly has noAlign = true
* },
* {
* role: 'section'
* text: 'Text2',
* fontSize: '20px',
* textType: 'normal'
* },
* {
* role: 'group',
* anchorItem: itemRef,
* items: [...]
* }
* ]
* ]
* }
* @class
*/
Kekule.Render.RichTextUtils = {
/** @private */
STYLE_PROPS: ['fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'color', 'opacity'],
/**
* Create a new and empty rich text object.
*/
create: function()
{
//return {'items': []};
return Kekule.Render.RichTextUtils.createGroup(/*'seq'*/);
},
/**
* Create a new group.
* @param {String} role Role of group, if not set, a normal role of 'group' will be created.
*/
createGroup: function(role, style)
{
var result = {'role': role || 'group', 'items': []};
if (style)
{
for (var p in style)
{
if (style.hasOwnProperty(p))
{
result[p] = style[p];
}
}
}
return result;
},
/**
* Create a section object of richText (an item in richtext array).
* @param {String} text
* @param {Hash} style
*/
createSection: function(text, style)
{
var section = {'text': text};
if (style)
{
for (var p in style)
{
if (style.hasOwnProperty(p))
{
section[p] = style[p];
}
}
}
return section;
},
/**
* Convert a plain string to rich format text.
* Multiline is supported.
* @param {String} str
* @param {Hash} style Can be null
* @returns {Object}
*/
strToRichText: function(str, style)
{
if (!str)
str = '';
var RTU = Kekule.Render.RichTextUtils;
var lines = str.split('\n');
//console.log('split to', lines);
if (lines.length <= 1)
return Kekule.Render.RichTextUtils.appendText(RTU.create(), str, style);
else // multiline
{
var result = RTU.createGroup(Kekule.Render.RichText.LINES);
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i] || '\u00a0'; // unicode non-break blank, to insert a blank line // TODO: may need a better solution
RTU.appendText(result, line, style);
}
//console.log('rich text', result);
return result;
}
},
/**
* Insert a styled text to a special position of richText group.
* @param {Object} richTextGroup
* @param {Int} index
* @param {String} text
* @param {Hash} style Can be null.
* @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
* @returns {Object}
*/
insertText: function(richTextGroup, index, text, style, isAnchor)
{
var section = Kekule.Render.RichTextUtils.createSection(text, style);
//richText.splice(index, 0, section);
richTextGroup.items.splice(index, 0, section);
if (isAnchor)
richTextGroup.anchorItem = section;
return richTextGroup;
},
/**
* Append a styled text to richText group and returns the whole group.
* @param {Object} richTextGroup
* @param {String} text
* @param {Hash} style Can be null.
* @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
* @returns {Object} richTextGroup
*/
appendText: function(richTextGroup, text, style, isAnchor)
{
var section = Kekule.Render.RichTextUtils.createSection(text, style);
richTextGroup.items.push(section);
if (isAnchor)
richTextGroup.anchorItem = section;
return richTextGroup;
},
/**
* Append a styled text to richText group and returns the new section created.
* @param {Object} richTextGroup
* @param {String} text
* @param {Hash} style Can be null.
* @param {Bool} isAnchor Whether the newly insert section will become the anchorItem.
* @returns {Object} New section appended to richTextGroup.
*/
appendText2: function(richTextGroup, text, style, isAnchor)
{
var section = Kekule.Render.RichTextUtils.createSection(text, style);
richTextGroup.items.push(section);
if (isAnchor)
richTextGroup.anchorItem = section;
return section;
},
/**
* Insert a group or section to a special position in destGroup.
* @param {Object} destGroup
* @param {Int} index
* @param {Object} groupOrSection
* @returns {Object}
*/
insert: function(destGroup, index, groupOrSection)
{
destGroup.items.splice(index, 0, groupOrSection);
return destGroup;
},
/**
* Append a group or section to tail in destGroup.
* @param {Object} destGroup
* @param {Object} groupOrSection
* @returns {Object}
*/
append: function(destGroup, groupOrSection)
{
destGroup.items.push(groupOrSection);
return destGroup;
},
/**
* Append a set of groups or sections to tail in destGroup.
* @param {Object} destGroup
* @param {Array} items
* @returns {Object}
*/
appendItems: function(destGroup, items)
{
destGroup.items = destGroup.items.concat(Kekule.ArrayUtils.toArray(items));
return destGroup;
},
/**
* Returns type (section or group) of item.
* @param {Object} item
* @returns {String} Constant value from {@link Kekule.Render.RichText}.
*/
getItemType: function(item)
{
if (item.items)
{
//return Kekule.Render.RichText.GROUP;
return item.role; // GROUP or LINE
}
else
{
return Kekule.Render.RichText.SECTION;
}
},
/**
* Returns role (normal, sup or sub) of item.
* @param {Object} item
* @returns {String} Constant value from {@link Kekule.Render.RichText}
*/
getItemRole: function(item)
{
//return /*item.role || Kekule.Render.RichText.NORMAL;*/
var R = Kekule.Render.RichText;
return (Kekule.Render.RichTextUtils.getItemType(item) === R.GROUP)? R.GROUP: R.SECTION;
},
/**
* Return text type (normal, superscript or subscript) of item.
* @param {Object} item
* @returns {String} Constant value from {@link Kekule.Render.RichText}
*/
getItemTextType: function(item)
{
return item.textType || Kekule.Render.RichText.NORMAL;
},
/**
* Check if item is a superscript.
* @param {Object} item
* @returns {Bool}
*/
isSuperscript: function(item)
{
return Kekule.Render.RichTextUtils.getItemTextType(item) == Kekule.Render.RichText.SUP;
},
/**
* Check if item is a superscript.
* @param {Object} item
* @returns {Bool}
*/
isSubscript: function(item)
{
return Kekule.Render.RichTextUtils.getItemTextType(item) == Kekule.Render.RichText.SUB;
},
/**
* Check if an item is a rich text group.
* @param {Object} item
* @returns {Bool}
*/
isGroup: function(item)
{
return Kekule.Render.RichTextUtils.getItemType(item) === Kekule.Render.RichText.GROUP;
},
/**
* Check if an item is a rich text section.
* @param {Object} item
* @returns {Bool}
*/
isSection: function(item)
{
return Kekule.Render.RichTextUtils.getItemType(item) === Kekule.Render.RichText.SECTION;
},
/**
* Returns the first normal text (nor sub/superscript) in rich text.
* @param {Object} richTextGroup
* @returns {Object}
*/
getFirstNormalTextSection: function(richTextGroup)
{
for (var i = 0, l = richTextGroup.items.length; i < l; ++i)
{
var item = richTextGroup.items[i];
var textType = Kekule.Render.RichTextUtils.getItemTextType(item);
if (textType === Kekule.Render.RichText.NORMAL)
return item;
}
return null;
},
/**
* Returns the actual refItem of item. Generally this function returns item.refItem,
* however, if that value is not set, function will return item's nearest sibling.
* @param {Object} item
* @param {Object} parent item's parent group.
* @returns {Object}
*/
getActualRefItem: function(item, parent)
{
var result = item.refItem;
if ((!result) && (parent.items.length > 1)) // check sibling
{
var index = parent.items.indexOf(item);
if (index > 0)
result = parent.items[index - 1];
else if (index == 0)
result = parent.items[1];
else
result = null;
}
return result;
},
/**
* Find the real anchor item in richText cascadely.
* @param {Object} richText
* @returns {Object}
*/
getFinalAnchorItem: function(richText)
{
if (richText.anchorItem)
return Kekule.Render.RichTextUtils.getFinalAnchorItem(richText.anchorItem);
else
return richText;
},
/**
* Tidy the rich text and merge groups with same style.
* @param {Object} richText
* @returns {Object}
*/
tidy: function(richText)
{
var result = Kekule.Render.RichTextUtils.createGroup(richText.role);
var currIndex = -1;
for (var i = 0, l = richText.items.length; i < l; ++i)
{
var item = richText.items[i];
if (item.items) // is group
{
var newGroup = Kekule.Render.RichTextUtils.tidy(item);
// check if newGroup has only one section, if true, convert it into a section
if (newGroup.items.length == 1)
{
var newItem = Object.extend({}, newGroup);
delete newItem.items;
delete newItem.role;
newItem = Object.extend(newItem, newGroup.items[0]);
item = newItem;
// then try merge items
}
else if (newGroup.items.length > 0)
{
++currIndex;
result.items.push(newGroup);
continue;
}
}
//else // text item, try merge
{
var merged = false;
if (currIndex >= 0)
{
var prevItem = result.items[currIndex];
if (!prevItem.items) // not group, just text
{
// check if item and prevItem has the same style
if (Kekule.ObjUtils.equal(item, prevItem, ['text']))
{
prevItem.text += item.text;
merged = true;
}
}
}
if (!merged)
{
++currIndex;
result.items.push(Object.extend({}, item));
}
}
}
return result;
},
/**
* Clone source rich text.
* @param {Object} richText
* @returns {Object}
*/
clone: function(richText)
{
var result = {};
Object.extend(result, richText);
if (richText.items)
{
result.items = [];
for (var i = 0, l = richText.items.length; i < l; ++i)
{
var item = {};
item = Kekule.Render.RichTextUtils.clone(richText.items[i]);
result.items.push(item);
/*
if (richText.anchorItem && (richText.anchorItem == richText.items[i]))
result.anchorItem = item;
*/
}
}
// RefItem and AnchorItem pointer should be handled carefully
if (result.items)
{
if (richText.anchorItem)
{
var index = richText.items.indexOf(richText.anchorItem);
if (index >= 0)
result.anchorItem = result.items[index]
else
result.anchorItem = null;
}
for (var i = 0, l = result.items.length; i < l; ++i)
{
var originItem = richText.items[i];
var item = result.items[i];
if (originItem.refItem)
{
var index = richText.items.indexOf(originItem.refItem);
if (index >= 0)
item.refItem = result.items[index]
else
item.refItem = null;
}
}
}
return result;
},
/**
* Extract text in rich text and returns a pure string.
* @param {Object} richText
* @returns {String}
*/
toText: function(richText)
{
var result = '';
if (richText.items)
{
for (var i = 0, l = richText.items.length; i < l; ++i)
{
var item = richText.items[i];
if (item.items) // group
result += Kekule.Render.RichTextUtils.toText(item);
else if (item.text) // plain text
{
result += item.text;
}
}
}
else
return richText.text || '';
return result;
},
/** @private */
_toDebugHtml: function(richText)
{
if (!richText.items) // only one text part
return richText.text;
var result = '';
for (var i = 0, l = richText.items.length; i < l; ++i)
{
var item = richText.items[i];
if (item.items) // group
result += Kekule.Render.RichTextUtils._toDebugHtml(item);
else if (item.text) // plain text
{
switch (item.textType)
{
case Kekule.Render.RichText.SUB:
result += '<sub>' + item.text + '</sub>';
break;
case Kekule.Render.RichText.SUP:
result += '<sup>' + item.text + '</sup>';
break;
default:
result += item.text;
}
}
}
return result;
},
/**
* Convert rich text to HTML code in a simple way.
* @param {Object} richText
* @returns {HTMLElement}
*/
toSimpleHtmlCode: function(richText)
{
return Kekule.Render.RichTextUtils._toDebugHtml(richText);
},
/**
* Create new rich text from HTML code.
* Note this method only handles flat <sup>/<sub> (with no nests) and normal text.
* @param {String} htmlCode
* @returns {Object} Created richtext object.
*/
fromSimpleHtmlCode: function(htmlCode)
{
var RT = Kekule.Render.RichText;
//var tagPattern = /\<\/?([a-z]+)\/?\>/i;
var tagPattern = new RegExp('\<\/?([a-z]+)\/?\>', 'ig');
// split the html code to a series of tags/endTags and innerTexts
var textItems = [];
var currIndex = 0;
var currTextType = RT.NORMAL;
var matchResult;
while ((matchResult = tagPattern.exec(htmlCode)) !== null)
{
var matchIndex = matchResult.index;
var sTag = matchResult[0];
var tagName = matchResult[1].toLowerCase();
if (currIndex < matchIndex) // there is something before this tag, should be simple text
textItems.push({'textType': currTextType, 'text': htmlCode.substring(currIndex, matchIndex)});
var isCloseTag = sTag.indexOf('/') === 1;
var isSelfCloseTag = sTag.lastIndexOf('/') === sTag.length - 2;
if (!isSelfCloseTag)
{
var tokenTagType = (tagName === 'sup')? RT.SUP:
(tagName === 'sub')? RT.SUB: null;
if (tokenTagType)
{
if (!isCloseTag)
currTextType = tokenTagType;
else if (currTextType === tokenTagType) // close a sup or sub
currTextType = RT.NORMAL;
}
}
currIndex = matchIndex + sTag.length;
}
if (htmlCode.length > currIndex)
{
textItems.push({'textType': currTextType, 'text': htmlCode.substring(currIndex)});
}
// generate the rich text
var RTU = Kekule.Render.RichTextUtils;
var result = RTU.createGroup(null, {'charDirection': Kekule.Render.TextDirection.LTR});
for (var i = 0, l = textItems.length; i < l; ++i)
{
var textType = textItems[i].textType;
var text = textItems[i].text;
if (text)
{
var child = RTU.createSection(text, {'textType': textType});
RTU.append(result, child);
}
}
return result;
},
/**
* Convert rich text to HTML element.
* Note: in the conversion, vertical lines are all turned into horizontal lines
* @param {Document} doc Owner document of result element.
* @param {Object} richText
* @param {Bool} reversedDirection
* @returns {HTMLElement}
*/
toHtml: function(doc, richText, reversedDirection)
{
var RT = Kekule.Render.RichText;
var result;
var reversed;
if (richText.charDirection)
reversed = richText.charDirection === Kekule.Render.TextDirection.RTL
|| richText.charDirection === Kekule.Render.TextDirection.BTT;
else
reversed = reversedDirection;
var role = richText.role;
if (role === RT.SECTION || richText.text) // text section
{
var textType = richText.textType;
var tagName = (textType === RT.SUB)? 'sub':
(textType === RT.SUP)? 'sup':
'span';
result = doc.createElement(tagName);
var text = richText.text;
if (reversed)
text = text.reverse();
Kekule.DomUtils.setElementText(result, richText.text || '');
}
else // line or group
{
var childEmbedTagName = null;
var childEmbedStyleText;
if (role === RT.LINES)
{
result = doc.createElement('div');
childEmbedTagName = 'p';
childEmbedStyleText = 'margin:0.2em 0;padding:0';
}
else // group
{
result = doc.createElement('span');
}
// convert children
var lastChildElem;
for (var i = 0, l = richText.items.length; i < l; ++i)
{
var item = richText.items[i];
var childElem = Kekule.Render.RichTextUtils.toHtml(doc, item, reversed);
if (childEmbedTagName)
{
var embedElem = doc.createElement(childEmbedTagName);
if (childEmbedStyleText)
embedElem.style.cssText = childEmbedStyleText;
embedElem.appendChild(childElem);
childElem = embedElem;
}
if (!reversed || !lastChildElem)
result.appendChild(childElem);
else
result.insertBefore(childElem, lastChildElem);
lastChildElem = childElem;
}
}
// styles
var elemStyle = result.style;
var styleProps = Kekule.Render.RichTextUtils.STYLE_PROPS;
for (var i = 0, l = styleProps.length; i < l; ++i)
{
var prop = styleProps[i];
if (richText[prop])
{
elemStyle[prop] = richText[prop];
}
}
return result;
},
/**
* Create new rich text from HTML element.
* @param {Element} htmlElement
* @returns {Object} Created richtext object.
*/
fromHtml: function(htmlElement)
{
var RT = Kekule.Render.RichText;
var RTU = Kekule.Render.RichTextUtils;
var DU = Kekule.DomUtils;
var SU = Kekule.StyleUtils;
function _createRichTextFromHtmlNode(node, isRoot)
{
if (node.nodeType === Node.TEXT_NODE) // pure text node, create a section
{
var text = node.nodeValue;
// erase line breaks
text = text.replace(/[\r\n]/g, '');
return text.trim()? RTU.createSection(text): null; // ignore all space text
}
else if (node.nodeType === Node.ELEMENT_NODE)
{
var stylePropNames = RTU.STYLE_PROPS;
stylePropNames.push('direction');
var styles = {};
// extract styles from HTML
for (var i = 0, l = stylePropNames.length; i < l; ++i)
{
var stylePropName = stylePropNames[i];
var value;
if (isRoot) // is root element, consider computed styles
{
value = SU.getComputedStyle(node, stylePropName);
}
else // child elements, only consider inline styles
{
value = node.style[stylePropName];
}
if (value)
styles[stylePropName] = value;
}
// char direction
if (!styles.direction)
styles.charDirection = Kekule.Render.TextDirection.DEFAULT;
else
{
styles.charDirection = (styles.direction === 'ltr')? Kekule.Render.TextDirection.LTR:
(styles.direction === 'rtl')? Kekule.Render.TextDirection.RTL:
(styles.direction === 'inherit')? Kekule.Render.TextDirection.INHERIT:
null;
delete styles.direction;
}
// Check if element has children elements
if (!DU.getFirstChildElem(node)) // no child element, may elem only contains text
{
var text = DU.getElementText(node);
var tagName = node.tagName.toLowerCase();
if (tagName === 'sub')
styles.textType = RT.SUB;
else if (tagName === 'sup')
styles.textType = RT.SUP;
else
styles.textType = RT.NORMAL;
return text? RTU.createSection(text, styles): null;
}
else // iterate through children
{
var resultRole = RT.GROUP;
var childNodes = DU.getChildNodesOfTypes(node, [Node.ELEMENT_NODE, Node.TEXT_NODE]);
var childRTs = [];
var hasLines = false;
for (var i = 0, l = childNodes.length; i < l; ++i)
{
var child = childNodes[i];
var childRT = _createRichTextFromHtmlNode(child, false);
if (child.nodeType === Node.ELEMENT_NODE) //
{
if (child.tagName.toLowerCase() === 'br') // explicit line break
{
// push special flags
childRTs.push('br');
hasLines = true;
}
var isBlockElem = SU.isBlockElem(child);
if (isBlockElem)
{
hasLines = true;
childRT._isLine = true; // markup it is a text line
}
}
if (childRT)
{
childRTs.push(childRT);
//RTU.append(result, childRT);
}
}
if (hasLines) // need to group childRTs into lines
{
var linedChildRTs = [];
var unpushedChildren = [];
for (var i = 0, l = childRTs.length; i < l; ++i)
{
var childRT = childRTs[i];
if (childRT._isLine || childRT == 'br') // special break flag
{
if (unpushedChildren.length) // create a new group to include all unhandled inline sections
{
if (unpushedChildren.length > 1)
{
var prevGroup = RTU.createGroup();
RTU.appendItems(prevGroup, unpushedChildren);
linedChildRTs.push(prevGroup);
}
else
linedChildRTs.push(unpushedChildren[0]);
unpushedChildren = [];
}
if (childRT._isLine)
{
delete childRT._isLine;
linedChildRTs.push(childRT);
}
}
else
unpushedChildren.push(childRT);
}
if (unpushedChildren.length)
{
if (unpushedChildren.length > 1)
{
var prevGroup = RTU.createGroup();
RTU.appendItems(prevGroup, unpushedChildren);
linedChildRTs.push(prevGroup);
}
else
linedChildRTs.push(unpushedChildren[0]);
}
childRTs = linedChildRTs;
}
// at last push childRTs to result
if (hasLines)
resultRole = RT.LINES;
var result = RTU.createGroup(resultRole, styles);
for (var i = 0, l = childRTs.length; i < l; ++i)
{
RTU.append(result, childRTs[i]);
}
return result;
}
}
}
return _createRichTextFromHtmlNode(htmlElement, true);
}
};
/**
* Methods about chem information displaying and rich text.
* @class
*/
Kekule.Render.ChemDisplayTextUtils = {
/** @private */
//RADICAL_LABELS: ['', '••', '•', '••'],
RADICAL_LABELS: ['', '\u2022\u2022', '\u2022', '\u2022\u2022'],
RADICAL_TRIPLET_ALTER_LABEL: '^^',
/**
* Returns suitable text to indicate the radical.
* @param {Int} radical
* @returns {String}
*/
getRadicalDisplayText: function(radical, useAlterTripletRadicalMark)
{
if (useAlterTripletRadicalMark && radical === Kekule.RadicalOrder.TRIPLET)
return Kekule.Render.ChemDisplayTextUtils.RADICAL_TRIPLET_ALTER_LABEL;
else
return Kekule.Render.ChemDisplayTextUtils.RADICAL_LABELS[radical];
},
/**
* Returns text to represent atom charge and radical (e.g., 2+).
* @param {Number} charge
* @param {Int} partialChargeDecimalsLength
* @param {Int} chargeMarkType
* @returns {String}
*/
getChargeDisplayText: function(charge, partialChargeDecimalsLength, chargeMarkType)
{
var slabel = '';
var showCharge = (!!charge) && (!partialChargeDecimalsLength || (Math.abs(charge) > Math.pow(10, -partialChargeDecimalsLength)/2));
if (showCharge)
{
var chargeSign = (charge > 0)? '+': '-';
var chargeAmount = Math.abs(charge);
if (chargeAmount != 1)
{
slabel += partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString();
}
else // +1 or -1, may use different charge sign char
{
if (chargeMarkType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND)
chargeSign = (charge > 0)? '\u2295': '\u2296';
}
slabel += chargeSign;
}
return slabel;
},
/**
* Returns text to represent atom electronic bias charge (e.g., δ+).
* @param {Int} electronicBias
* @param {String} electronicBiasMark
* @returns {String}
*/
getElectronicBiasDisplayText: function(electronicBias, electronicBiasMark)
{
var sLabel = '';
if (electronicBias && electronicBiasMark)
{
var chargeSign = (electronicBias > 0)? '+':
(electronicBias < 0)? '-': null;
if (chargeSign)
{
sLabel = electronicBiasMark;
var count = Math.abs(electronicBias);
for (var i = 0; i < count; ++i)
sLabel += chargeSign;
}
}
return sLabel;
},
/**
* Returns text to represent atom charge (e.g., 2+).
* @param {Float} charge
* @param {Int} electronicBias
* @param {Int} partialChargeDecimalsLength
* @param {String} electronicBiasMark
* @param {Int} chargeMarkType
* @returns {String}
*/
getChargeExDisplayText: function(charge, electronicBias, partialChargeDecimalsLength, electronicBiasMark, chargeMarkType)
{
if (charge) // usual presice charge
return Kekule.Render.ChemDisplayTextUtils.getChargeDisplayText(charge, partialChargeDecimalsLength, chargeMarkType);
else if (electronicBias) // delta+ or delta-
return Kekule.Render.ChemDisplayTextUtils.getElectronicBiasDisplayText(electronicBias, electronicBiasMark);
else
return '';
},
/**
* Create a rich text section (usually superscript) to display atom charge and radical.
* @param {Number} charge
* @param {Int} radical
* @param {Int} partialChargeDecimalsLength
* @param {Int} chargeMarkType
* @param {Int} electronicBias
* @param {String} electronicBiasMark
* @returns {Object}
*/
createElectronStateDisplayTextSection: function(charge, radical, partialChargeDecimalsLength, chargeMarkType, useAlterTripletRadicalMark, electronicBias, electronicBiasMark)
{
var result = null;
var slabel = '';
/*
var showCharge = (!!charge) && (!partialChargeDecimalsLength || (Math.abs(charge) > Math.pow(10, -partialChargeDecimalsLength)/2));
if (showCharge)
{
var chargeSign = (charge > 0)? '+': '-';
var chargeAmount = Math.abs(charge);
if (chargeAmount != 1)
{
slabel += partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString();
}
else // +1 or -1, may use different charge sign char
{
if (chargeMarkType === Kekule.Render.ChargeMarkRenderType.CIRCLE_AROUND)
chargeSign = (charge > 0)? '\u2295': '\u2296';
}
slabel += chargeSign;
}
*/
if (charge || electronicBias)
{
// slabel = Kekule.Render.ChemDisplayTextUtils.getChargeDisplayText(charge, partialChargeDecimalsLength, chargeMarkType);
slabel = Kekule.Render.ChemDisplayTextUtils.getChargeExDisplayText(charge, electronicBias, partialChargeDecimalsLength, electronicBiasMark, chargeMarkType);
}
if (radical)
{
slabel += Kekule.Render.ChemDisplayTextUtils.getRadicalDisplayText(radical, useAlterTripletRadicalMark) || '';
}
if (slabel)
result = Kekule.Render.RichTextUtils.createSection(slabel,
{'textType': Kekule.Render.RichText.SUP, 'charDirection': Kekule.Render.TextDirection.LTR}
);
return result;
},
/**
* Convert a chemistry formula to a displayable rich format text label.
* @param {Kekule.MolecularFormula} formula
* @param {Bool} showCharge Whether display formula charge.
* @param {Bool} showRadical Whether display formula radical.
* @param {Int} partialChargeDecimalsLength
* @returns {Object}
*/
formulaToRichText: function(formula, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical)
{
//var result = Kekule.Render.RichTextUtils.create();
var result = Kekule.Render.ChemDisplayTextUtils._convFormulaToRichTextGroup(formula, false, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical);
return result;
},
/** @private */
_convFormulaToRichTextGroup: function(formula, showBracket, showCharge, showRadical, partialChargeDecimalsLength, displayConfigs, chargeMarkType, distinguishSingletAndTripletRadical)
{
var result = Kekule.Render.RichTextUtils.createGroup();
var sections = formula.getSections();
if (showBracket)
{
var bracketIndex = formula.getMaxNestedLevel() % Kekule.FormulaUtils.FORMULA_BRACKET_TYPE_COUNT;
var bracketStart = Kekule.FormulaUtils.FORMULA_BRACKETS[bracketIndex][0];
var bracketEnd = Kekule.FormulaUtils.FORMULA_BRACKETS[bracketIndex][1];
result = Kekule.Render.RichTextUtils.appendText(result, bracketStart);
}
for (var i = 0, l = sections.length; i < l; ++i)
{
var obj = sections[i].obj;
var charge = formula.getSectionCharge(sections[i]);
var subgroup = null;
if (obj instanceof Kekule.MolecularFormula) // a sub-formula
{
// TODO: sometimes bracket is unessential, such as SO42- and so on, need more judge here
subgroup = Kekule.Render.ChemDisplayTextUtils._convFormulaToRichTextGroup(obj, true, false, false, partialChargeDecimalsLength, displayConfigs, chargeMarkType); // do not show charge right after, we will add it later
}
else if (obj.getDisplayRichText) // an atom/isotope
{
var subgroup = obj.getDisplayRichText(Kekule.Render.HydrogenDisplayLevel.NONE, false, null, displayConfigs, partialChargeDecimalsLength, chargeMarkType); // do not show charge right after symbol
}
if (subgroup)
{
// count
if (sections[i].count != 1)
{
subgroup = Kekule.Render.RichTextUtils.appendText(subgroup, sections[i].count.toString(), {'textType': Kekule.Render.RichText.SUB});
subgroup.charDirection = Kekule.Render.TextDirection.LTR;
}
// charge is draw after count
if (showCharge && charge)
{
var chargeSection = Kekule.Render.ChemDisplayTextUtils.createElectronStateDisplayTextSection(charge, null, partialChargeDecimalsLength, chargeMarkType, distinguishSingletAndTripletRadical);
if (chargeSection)
{
Kekule.Render.RichTextUtils.append(subgroup, chargeSection);
}
}
result = Kekule.Render.RichTextUtils.append(result, subgroup);
}
}
if (showBracket)
result = Kekule.Render.RichTextUtils.appendText(result, bracketEnd);
if (showCharge || showRadical)
{
var charge = formula.getCharge();
var radical = formula.getRadical();
var chargeSection = Kekule.Render.ChemDisplayTextUtils.createElectronStateDisplayTextSection(charge, radical, partialChargeDecimalsLength, chargeMarkType);
if (chargeSection)
{
Kekule.Render.RichTextUtils.append(result, chargeSection);
}
}
return result;
}
};
/**
* Help methods to draw text.
* @class
*/
Kekule.Render.TextDrawUtils = {
/**
* Check if a text line is in horizontal direction (LTR or RTL).
* @param {Object} charDirection
* @returns {Bool}
*/
isHorizontalLine: function(charDirection)
{
return ((charDirection == Kekule.Render.TextDirection.LTR)
|| (charDirection == Kekule.Render.TextDirection.RTL)
|| (charDirection == null)); // default is horizontal
},
/**
* Check if a text line is in vertical direction (TTB or BTT).
* @param {Object} charDirection
* @returns {Bool}
*/
isVerticalLine: function(charDirection)
{
return ((charDirection == Kekule.Render.TextDirection.TTB)
|| (charDirection == Kekule.Render.TextDirection.BTT));
},
/**
* Get opposite direction.
* @param {Int} direction
* @returns {Int}
*/
getOppositeDirection: function(direction)
{
var TD = Kekule.Render.TextDirection;
switch (direction)
{
case TD.LTR: return TD.RTL;
case TD.RTL: return TD.LTR;
case TD.TTB: return TD.BTT;
case TD.BTT: return TD.TTB;
}
},
/**
* Check if two directions are opposite.
* @param {Int} direction1
* @param {Int} direction2
* @return {Bool}
*/
isOppositeDirection: function(direction1, direction2)
{
return Kekule.Render.TextDrawUtils.getOppositeDirection(direction1) === direction2;
},
/**
* Turn line break direction to actual direction value.
* @param {Int} direction
* @param {Int} parentDirection
*/
getActualDirection: function(direction, parentDirection)
{
var TD = Kekule.Render.TextDirection;
if (direction === TD.LINE_BREAK)
{
return ((parentDirection === TD.TTB) || (parentDirection === TD.BTT))? TD.LTR:
((parentDirection === TD.LTR) || (parentDirection === TD.RTL))? TD.TTB:
direction;
}
else
return direction;
},
getActualAlign: function(align, direction)
{
var A = Kekule.Render.TextAlgin;
var D = Kekule.Render.TextDirection;
switch (align)
{
case A.LEFT:
case A.RIGHT:
case A.TOP:
case A.BOTTOM:
case A.CENTER:
{
return align;
break;
}
case A.LEADING:
{
switch (direction)
{
case D.TTB: return A.TOP;
case D.BTT: return A.BOTTOM;
case D.RTL: return A.RIGHT;
case D.LTR:
default:
return A.LEFT;
}
}
case A.TRAILING:
{
switch (direction)
{
case D.BTT: return A.TOP;
case D.TTB: return A.BOTTOM;
case D.RTL: return A.LEFT;
case D.LTR:
default:
return A.RIGHT;
}
}
}
}
}
/**
* Help methods about path used in 2D renderer.
* @class
*/
Kekule.Render.DrawPathUtils = {
/**
* Make a path array from arguments.
* For instance, makePath('M', [10, 20], 'L', [20, 30]).
* The path method string is similiar to SVG path string format, including:
* M moveto (x y)+
* Z closepath (none)
* L lineto (x y)+
* //H horizontal lineto x+
* //V vertical lineto y+
* C curveto (x1 y1 x2 y2 x y)+
* S smooth curveto (x2 y2 x y)+
* Q quadratic Bézier curveto (x1 y1 x y)+
* T smooth quadratic Bézier curveto (x y)+
* A elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
*/
makePath: function()
{
var result = [];
var sMethod = null;
var params = [];
var group;
for (var i = 0, l = arguments.length; i < l; ++i)
{
var arg = arguments[i];
if (DataType.isArrayValue(arg))
{
params = params.concat(arg);
}
else if (typeof(arg) === 'number')
{
params.push(arg);
}
else // start of a new method
{
if (sMethod) // there is a prev method, group up it
{
group = {
'method': sMethod,
'params': params
};
result.push(group);
// empty curr method and params
sMethod = null;
params = [];
}
// then new method
sMethod = arg.toString();
}
if (i === l - 1) // last one, wrap up last group
{
if (sMethod) // there is a prev method, group up it
{
group = {
'method': sMethod,
'params': params
};
result.push(group);
}
}
}
return result;
}
};
/**
* Help methods to draw connector (usually a bond).
* @class
*/
Kekule.Render.ConnectorDrawUtils = {
/**
* Get possible render type of a connector.
* If connector is a bond, this function will return the same result as {@link Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderType},
* otherwise a default set will be returned.
* @param {Kekule.ChemStructureConnector} connector
* @returns {Array} A set of possible values from {@link Kekule.Render.BondRenderType}.
*/
getPossibleConnectorRenderTypes: function(connector)
{
if (connector instanceof Kekule.Bond)
return Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderTypes(connector);
else
{
var RT = Kekule.Render.BondRenderType;
return [RT.SINGLE, RT.DASHED, RT.WAVY];
}
},
/**
* Get default render type of a connector.
* @param {Kekule.ChemStructureConnector} connector
* @returns {Int} Value from {@link Kekule.Render.BondRenderType}.
*/
getDefConnectorRenderType: function(connector)
{
var a = Kekule.Render.ConnectorDrawUtils.getPossibleConnectorRenderTypes(connector);
return a[0];
},
/**
* Get possible render type of a bond.
* @param {Kekule.Bond} bond
* @returns {Array} A set of possible values from {@link Kekule.Render.BondRenderType}.
*/
getPossibleBondRenderTypes: function(bond)
{
var RT = Kekule.Render.BondRenderType;
var BT = Kekule.BondType;
var btype = bond.getBondType();
switch (btype)
{
case BT.HYDROGEN:
case BT.TRANSITION:
return [RT.DASHED, RT.ARROWED, RT.WAVY]; break;
// TODO: Arrow direction of coordinate bond should be calculated
case BT.COORDINATE: return [RT.ARROWED, RT.SINGLE, RT.DASHED, RT.WAVY]; break;
case BT.IONIC: /*case BT.COORDINATE:*/ case BT.METALLIC: case BT.UNKNOWN:
return [RT.SINGLE, RT.DASHED, RT.WAVY]; break;
case BT.COVALENT: break; // need further check in the following code
default:
return [RT.SINGLE, RT.DASHED, RT.ARROWED, RT.WAVY];
}
// if covalent bond, then further check it
if (bond.getConnectedObjCount() > 2) // multiple center bond
return [RT.DASHED, RT.SINGLE];
var BO = Kekule.BondOrder;
switch (bond.getBondOrder())
{
case BO.DOUBLE:
{
var dresult = [RT.DOUBLE, RT.DASHED_DOUBLE, RT.BOLD_DOUBLE, RT.WAVY];
if (bond.getStereo && [Kekule.BondStereo.E_OR_Z, Kekule.BondStereo.CIS_OR_TRANS].indexOf(bond.getStereo()) >= 0)
dresult.unshift(RT.SCISSORS_DOUBLE);
return dresult;
break;
}
case BO.TRIPLE: return [RT.TRIPLE, RT.BOLD_TRIPLE, RT.WAVY]; break;
case BO.QUAD: return [RT.QUAD, RT.BOLD_QUAD, RT.WAVY]; break;
case BO.EXPLICIT_AROMATIC: return [RT.SOLID_DASH, RT.WAVY]; break;
case BO.SINGLE: // complex, need consider stereo chemistry
{
var BS = Kekule.BondStereo;
var result = [RT.SINGLE, RT.BOLD, RT.WAVY];
switch (bond.getStereo())
{
case BS.UP: result.unshift(RT.WEDGED_SOLID, RT.WEDGED_HOLLOW); break;
case BS.UP_INVERTED: result.unshift(RT.WEDGED_SOLID_INV, RT.WEDGED_HOLLOW_INV); break;
case BS.DOWN: result.unshift(RT.WEDGED_HASHED, RT.DASHED); break;
case BS.DOWN_INVERTED: result.unshift(RT.WEDGED_HASHED_INV, RT.DASHED); break;
case BS.UP_OR_DOWN: case BS.UP_OR_DOWN_INVERTED: result = [RT.WAVY, RT.SINGLE]; break;
case BS.CLOSER: result = [RT.WEDGED_SOLID_BOTH, RT.WEDGED_HOLLOW_BOTH]; break;
default: // NONE
;// do nothing
}
return result;
break;
}
default: // OTHER, UNSET
return [RT.SINGLE, RT.DASHED, RT.ARROWED, RT.WAVY];
}
},
/**
* Get default render type of a bond.
* @param {Kekule.Bond} bond
* @returns {Int} Value from {@link Kekule.Render.BondRenderType}.
*/
getDefBondRenderType: function(bond)
{
var a = Kekule.Render.ConnectorDrawUtils.getPossibleBondRenderTypes(bond);
return a[0];
},
/**
* Get 3D render type of a connector.
* If connector is a bond, this function will return the same result as {@link Kekule.Render.ConnectorDrawUtils.getBondRender3DTypes},
* otherwise a default type will be returned.
* @param {Kekule.ChemStructureConnector} connector
* @param {Int} renderMode Value from {@link Kekule.Render.Bond3DRenderMode}.
* @returns {Int} Value from {@link Kekule.Render.Bond3DRenderType}.
*/
getConnectorRender3DType: function(connector, renderMode)
{
var BRT = Kekule.Render.Bond3DRenderType;
var BRM = Kekule.Render.Bond3DRenderMode;
if (renderMode && ((renderMode === BRM.WIRE) || (renderMode === BRM.CYLINDER)))
return BRT.SINGLE;
else
{
if (connector instanceof Kekule.Bond)
return Kekule.Render.ConnectorDrawUtils.getBondRender3DTypes(connector);
else
return BRT.SINGLE;
}
},
/**
* Get suitable 3D render type of a bond.
* @param {Kekule.Bond} bond
* @returns {Int} Value from {@link Kekule.Render.Bond3DRenderType}.
*/
getBondRender3DTypes: function(bond)
{
var RT = Kekule.Render.Bond3DRenderType;
var BT = Kekule.BondType;
var btype = bond.getBondType();
switch (btype)
{
case BT.HYDROGEN:
case BT.TRANSITION:
return RT.DASH; break;
case BT.COVALENT: break; // need further check in the following code
default:
return RT.SINGLE;
}
// if covalent bond, then further check it
var BO = Kekule.BondOrder;
switch (bond.getBondOrder())
{
case BO.DOUBLE: return RT.DOUBLE; break;
case BO.TRIPLE: return RT.TRIPLE; break;
case BO.EXPLICIT_AROMATIC: return RT.SOLID_DASH; break;
default: // OTHER, UNSET
return RT.SINGLE;
}
}
};
/**
* Help methods to manupilate chem object.
* @class
*/
Kekule.Render.ObjUtils = {
/**
* Returns 2D containing box of usual chem object.
* @param {Kekule.ChemObjecy} chemObj
* @param {Int} coordMode
* @param {Bool} allowCoordBorrow
* @returns {Hash}
*/
getContainerBox: function(chemObj, coordMode, allowCoordBorrow)
{
if (!coordMode)
coordMode = Kekule.CoordMode.COORD2D;
var box;
var o = chemObj;
/*
if (o.getExposedContainerBox2D)
box = o.getExposedContainerBox2D(allowCoordBorrow);
else if (o.getContainerBox2D)
box = o.getContainerBox2D(allowCoordBorrow);
else // no containerBox related method, use coord
box = null;
*/
if (o.getExposedContainerBox)
box = o.getExposedContainerBox(coordMode, allowCoordBorrow);
else if (o.getContainerBox)
box = o.getContainerBox(coordMode, allowCoordBorrow);
else // no containerBox related method, use coord
{
var coord = o.getAbsCoordOfMode? o.getAbsCoordOfMode(coordMode): null;
box = coord? Kekule.BoxUtils.createBox(coord, coord): null;
}
return box;
}
};
/**
* Help methods to manipulate bound info base on simple shape.
* @class
*/
Kekule.Render.MetaShapeUtils = {
//Kekule.Render.BoundUtils = {
/**
* Create a new ShapeInfo object.
* @param {Int} shapeType
* @param {Array} coords
* @param {Hash} additionalInfos
* @returns {Object}
*/
createShapeInfo: function(shapeType, coords, additionalInfos)
//createBoundInfo: function(boundType, coords, additionalInfos)
{
var result = {'shapeType': shapeType, 'coords': coords};
/*
result.shapeType = shapeType;
result.coords = coords;
*/
if (additionalInfos)
result = Object.extend(result, additionalInfos);
return result;
},
/**
* Check if shape is a composite one (usually is an array of simple shapes).
* @param {Variant} shape
* @returns {Bool}
*/
isCompositeShape: function(shape)
{
return DataType.isArrayValue(shape);
},
/**
* Inflate shape with delta on each direction.
* @param {Object} originalShape
* @param {Float} delta
* @returns {Object} A new boundInfo.
*/
inflateShape: function(originalShape, delta)
{
if (!originalShape)
return null;
var T = Kekule.Render.MetaShapeType;
var B = Kekule.Render.MetaShapeUtils;
// composite shape
if (B.isCompositeShape(originalShape))
{
var result = [];
for (var i = 0, l = originalShape.length; i < l; ++i)
{
var oldChildShape = originalShape[i];
var newChildShape = B.inflateShape(oldChildShape, delta);
result.push(newChildShape);
}
return result;
}
// simple shape
var newBound;
switch (originalShape.shapeType)
{
case T.POINT: newBound = B._inflatePointShape(originalShape, delta); break;
case T.CIRCLE: newBound = B._inflateCircleShape(originalShape, delta); break;
case T.LINE: newBound = B._inflateLineShape(originalShape, delta); break;
case T.RECT: newBound = B._inflateRectShape(originalShape, delta); break;
case T.POLYGON: newBound = B._inflatePolygonShape(originalShape, delta); break;
case T.ARC: newBound = B._inflateArcShape(originalShape, delta); break;
}
return newBound;
},
/** @private */
_inflatePointShape: function(originalShape, delta)
{
var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
Kekule.Render.BoundShapeType.CIRCLE, [originalShape.coords[0]], {'radius': delta});
return newBound;
},
/** @private */
_inflateCircleShape: function(originalShape, delta)
{
var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
Kekule.Render.BoundShapeType.CIRCLE, [originalShape.coords[0]], {'radius': originalShape.radius + delta});
return newBound;
},
/** @private*/
_inflateLineShape: function(originalShape, delta)
{
var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(
Kekule.Render.BoundShapeType.LINE,
[originalShape.coords[0], originalShape.coords[1]], {'width': (originalShape.width || 0) + delta * 2});
return newBound;
},
/** @private*/
_inflateArcShape: function(originalShape, delta)
{
var newBound = Object.extend({}, originalShape);
newBound.width = (originalShape.width || 0) + delta * 2;
return newBound;
},
/** @private */
_inflateRectShape: function(originalShape, delta)
{
var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(Kekule.Render.BoundShapeType.RECT, []);
var newCoords = Kekule.Render.MetaShapeUtils._inflateCoords(originalShape.coords, delta);
newBound.coords = newCoords;
return newBound;
},
/** @private */
_inflatePolygonShape: function(originalShape, delta)
{
var newBound = Kekule.Render.MetaShapeUtils.createShapeInfo(Kekule.Render.BoundShapeType.POLYGON, []);
var newCoords = Kekule.Render.MetaShapeUtils._inflateCoords(originalShape.coords, delta);
newBound.coords = newCoords;
return newBound;
},
/** @private */
_inflateCoords: function(coords, delta)
{
var C = Kekule.CoordUtils;
var result = [];
// calc center of coords
var center = C.getCenter(coords);
for (var i = 0, l = coords.length; i < l; ++i)
{
var coord = coords[i];
var vec = C.substract(coord, center);
var distance = C.getDistance(vec);
var deltaVec = C.multiply(vec, delta / distance);
var newCoord = C.add(coord, deltaVec);
result.push(newCoord);
}
return result;
},
/**
* Returns distance of coord to a bound shape. A negative value means coord insude shape.
* @param {Hash} coord
* @param {Hash} shapeInfo Provides the shape box information of this object on context. It has the following fields:
* {
* shapeType: value from {@link Kekule.Render.MetaShapeType}.
* coords: [Array of coords]
* otherInfo: ...
* }
* Note that shapeInfo may be an array, in that case, nearest distance will be returned.
* @param {Float} inflate
* @returns {Float}
*/
getDistance: function(coord, shapeInfo, inflate)
{
if (Kekule.Render.MetaShapeUtils.isCompositeShape(shapeInfo))
{
var result = null;
for (var i = 0, l = shapeInfo.length; i < l; ++i)
{
var info = shapeInfo[i];
var d = Kekule.Render.MetaShapeUtils.getDistance(coord, info, inflate);
if (result === null || result > d)
result = d;
}
return result;
}
else
{
var T = Kekule.Render.MetaShapeType;
var B = Kekule.Render.MetaShapeUtils;
var newBound = inflate? B.inflateShape(shapeInfo, inflate): shapeInfo;
switch (shapeInfo.shapeType)
{
case T.POINT: return (inflate? B._getDistanceToCircle(coord, newBound): B._getDistanceToPoint(coord, newBound));
case T.CIRCLE: return B._getDistanceToCircle(coord, newBound);
case T.LINE: return B._getDistanceToLine(coord, newBound);
case T.RECT: return B._getDistanceToRect(coord, newBound);
case T.POLYGON: return B._getDistanceToPolygon(coord, newBound);
case T.ARC: return B._getDistanceToArc(coord, newBound);
default: return false;
}
}
},
/** @private */
_getDistanceToPoint: function(coord, shapeInfo)
{
return Kekule.CoordUtils.getDistance(coord, shapeInfo.coords[0]);
},
/** @private */
_getDistanceToCircle: function(coord, shapeInfo)
{
var C = Kekule.CoordUtils;
var d = C.getDistance(coord, shapeInfo.coords[0]);
return d - shapeInfo.radius;
},
/** @private */
_getDistanceToArc: function(coord, shapeInfo)
{
var C = Kekule.CoordUtils;
var radiusVector = C.substract(coord, shapeInfo.coords[0]);
var currAngle = Math.atan2(radiusVector.y, radiusVector.x);
if (Kekule.Render.MetaShapeUtils.isAngleInArcRange(currAngle, shapeInfo.startAngle, shapeInfo.endAngle, shapeInfo.anticlockwise))
{
// coord in arc range, check distance to arc
v