UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,652 lines (1,602 loc) 103 kB
/** * @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