/**
* @fileoverview
* Utils method about chem objects.
* @author Partridge Jiang
*/
/*
* requires /utils/kekule.utils.js
* requires /core/kekule.common.js
* requires /kekule.structures.js
*/
(function () {
"use strict";
var AU = Kekule.ArrayUtils;
/**
* Util class to manipulate ctab based chem structures.
* @class
*/
Kekule.ChemStructureUtils = {
/**
* Returns median of all input connector lengths.
* @param {Array} connectors
* @param {Int} coordMode
* @param {Bool} allowCoordBorrow
* @return {Float}
*/
getConnectorLengthMedian: function(connectors, coordMode, allowCoordBorrow)
{
var lengths = [];
for (var i = 0, l = connectors.length; i < l; ++i)
{
var connector = connectors[i];
if (connector && connector.getLength)
{
var length = connector.getLength(coordMode, allowCoordBorrow);
if (length)
lengths.push(length);
}
}
if (l === 0) // no connectors at all
return 1; // TODO: this value should be calculated
if (l <= 1)
return lengths[0];
else
{
// sort lengths to find the median one
lengths.sort();
var count = lengths.length;
var result = (count % 2)? lengths[(count + 1) >> 1]: (lengths[count >> 1] + lengths[(count >> 1) - 1]) / 2;
return result;
}
},
/**
* Returns structured children of chemObj. The type of chemObj can be:
* {@link Kekule.ChemObjList}: returns chemObj.getItems();
* {@link Kekule.ChemStructureObjectGroup}: returns chemObj.getAllObjs();
* {@link Kekule.CompositeMolecule}: returns chemObj.getSubMolecules().getAllObjs().
* {@link Kekule.ChemSpaceElement} or {@link Kekule.ChemSpace}: returns all child structured objects inside it.
* Other types will simply return [chemObj].
* If param cascade is true, each childObj will also be checked.
* @param {Variant} chemObj
* @param {Bool} cascade
* @returns {Array}
*/
getChildStructureObjs: function(chemObj, cascade)
{
var result;
if (chemObj instanceof Kekule.CompositeMolecule)
result = chemObj.getSubMolecules().getAllObjs();
else if (chemObj instanceof Kekule.ChemStructureObjectGroup)
result = chemObj.getAllObjs();
else if (chemObj instanceof Kekule.ChemObjList)
result = chemObj.getItems();
else if (chemObj instanceof Kekule.ChemSpaceElement)
result = chemObj.getChildren().getItems();
else if (chemObj instanceof Kekule.ChemSpace)
result = chemObj.getChildren();
else
{
return [chemObj];
}
result = [].concat(result); // clone result, avoid affect properties of chemObj
// if not returned and cascade, need future check
if (cascade)
{
var newResult = [];
for (var i = 0, l = result.length; i < l; ++i)
{
var obj = result[i];
var cascadeChilds = Kekule.ChemStructureUtils.getChildStructureObjs(obj, cascade);
if (cascadeChilds.length <= 1) // can not find cascade children
Kekule.ArrayUtils.pushUnique(newResult, obj);
else // children find, use them to replace obj
{
Kekule.ArrayUtils.pushUnique(newResult, cascadeChilds);
}
}
result = newResult;
}
//console.log(result);
return result;
},
/**
* Returns all child structure fragments among children of chemObj.
* @param {Variant} chemObj
* @param {Bool} cascade
* @returns {Array}
*/
getAllStructFragments: function(chemObj, cascade)
{
if (chemObj instanceof Kekule.StructureFragment)
return [chemObj];
var childObjs = Kekule.ChemStructureUtils.getChildStructureObjs(chemObj, cascade);
var result = [];
for (var i = 0, l = childObjs.length; i < l; ++i)
{
if (childObjs[i] instanceof Kekule.StructureFragment)
Kekule.ArrayUtils.pushUnique(result, childObjs[i]);
}
return result;
},
/**
* Find all child structure fragments among children of chemObj, then merge them into one.
* @param {Variant} chemObj
* @param {Class} newFragmentClass If set, new fragment will be based on this class.
* Otherwise an instance of {@link Kekule.Molecule} will be created.
* @return {Kekule.StructureFragment}
*/
getTotalStructFragment: function(chemObj, newFragmentClass)
{
var fragments = Kekule.ChemStructureUtils.getAllStructFragments(chemObj, true);
var count = fragments.length;
if (count <= 0) // nothing found
return null;
else if (count === 1) // only one, returns it directly
return fragments[0];
else // need merge
{
return Kekule.ChemStructureUtils.mergeStructFragments(fragments, newFragmentClass);
}
},
/**
* Returns nodes or connectors that should be removed cascadely with chemStructObj.
* @param {Object} chemStructObj
* @returns {Array}
* @deprecated
*/
getCascadeDeleteObjs: function(chemStructObj)
{
var result = [];
// all usual connectors (two ends) connected to chemStructObj should be removed
var linkedConnectors = chemStructObj.getLinkedConnectors? chemStructObj.getLinkedConnectors(): [];
for (var i = 0, l = linkedConnectors.length; i < l; ++i)
{
var connector = linkedConnectors[i];
if (connector.getConnectedObjs().length <= 2)
{
Kekule.ArrayUtils.pushUnique(result, connector);
var newCascadeObjs = Kekule.ChemStructureUtils.getCascadeDeleteObjs(connector);
Kekule.ArrayUtils.pushUnique(result, newCascadeObjs);
}
}
if (chemStructObj instanceof Kekule.ChemStructureNode)
{
// no additional objects should be delete
}
else if (chemStructObj instanceof Kekule.ChemStructureConnector)
{
// nodes connected with and only with this connector should be removed
var objs = chemStructObj.getConnectedObjs();
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj instanceof Kekule.ChemStructureNode)
{
if (obj.getLinkedConnectors().length <= 1)
Kekule.ArrayUtils.pushUnique(result, obj);
}
}
}
else // other objects
;
return result;
},
/**
* Move nodes and connectors from target to dest structure fragment.
* @param {Kekule.StructureFragment} target
* @param {Kekule.StructureFragment} dest
* @param {Array} moveNodes
* @param {Array} moveConnectors
* @param {Bool} ignoreAnchorNodes
*/
moveChildBetweenStructFragment: function(target, dest, moveNodes, moveConnectors, ignoreAnchorNodes)
{
/*
var CU = Kekule.CoordUtils;
target.beginUpdate();
dest.beginUpdate();
var anchorNodes = target.getAnchorNodes();
try
{
// TODO: here we need change coord if essential
var targetCoord2D = target.getAbsCoord2D();
var targetCoord3D = target.getAbsCoord3D();
var destCoord2D = dest.getAbsCoord2D();
var destCoord3D = dest.getAbsCoord3D();
var coordDelta2D = CU.substract(targetCoord2D, destCoord2D);
var coordDelta3D = CU.substract(targetCoord3D, destCoord3D);
//console.log('coordDelta', coordDelta2D, coordDelta3D);
var nodes = Kekule.ArrayUtils.clone(moveNodes);
var connectors = Kekule.ArrayUtils.clone(moveConnectors);
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var index = target.indexOfNode(node);
if (index >= 0)
{
target.removeNodeAt(index, true); // preserve linked connectors
var oldCoord2D = node.getCoord2D();
if (oldCoord2D)
{
var newCoord2D = CU.add(oldCoord2D, coordDelta2D);
node.setCoord2D(newCoord2D);
}
var oldCoord3D = node.getCoord3D();
if (oldCoord3D)
{
var newCoord3D = CU.add(oldCoord3D, coordDelta3D);
node.setCoord2D(newCoord3D);
}
dest.appendNode(node);
if (anchorNodes.indexOf(node)>= 0)
{
target.removeAnchorNode(node);
if (!ignoreAnchorNodes)
dest.appendAnchorNode(node);
}
}
}
for (var i = 0, l = connectors.length; i < l; ++i)
{
var connector = connectors[i];
var index = target.indexOfConnector(connector);
if (index >= 0)
{
target.removeConnectorAt(index, true); // preserve linked objects
dest.appendConnector(connector);
}
}
}
finally
{
//console.log('[struct merge done]');
dest.endUpdate();
target.endUpdate();
}
*/
return Kekule.StructureFragment.moveChildBetweenStructFragment(target, dest, moveNodes, moveConnectors, ignoreAnchorNodes);
},
/** @private */
_getCascadeConnectedNodesAndConnectors: function(connector, parentStructFragment)
{
var connectors = [];
var nodes = [];
var objs = connector.getConnectedObjs();
for (var j = 0, k = objs.length; j < k; ++j)
{
var obj = objs[j];
if (obj !== connector)
{
if (parentStructFragment)
obj = parentStructFragment.findDirectChildOfObj(obj);
if (obj instanceof Kekule.ChemStructureNode)
Kekule.ArrayUtils.pushUnique(nodes, obj);
else if (obj instanceof Kekule.ChemStructureConnector)
{
Kekule.ArrayUtils.pushUnique(connectors, obj);
var connected = Kekule.ChemStructureUtils._getCascadeConnectedNodesAndConnectors(obj);
Kekule.ArrayUtils.pushUnique(connectors, connected.connectors);
Kekule.ArrayUtils.pushUnique(nodes, connected.nodes);
}
}
}
return {'connectors': connectors, 'nodes': nodes};
},
/**
* Merge all fragments into a big one (this one may be unconnected).
* @param {Array} fragments
* @param {Class} newFragmentClass If set, new fragment will be based on this class.
* Otherwise an instance of {@link Kekule.Molecule} will be created.
* @return {Kekule.StructureFragment}
*/
mergeStructFragments: function(fragments, newFragmentClass)
{
if (fragments.length <= 1)
return fragments[0];
else
{
var fclass = newFragmentClass || Kekule.Molecule;
var result = new fclass();
for (var i = 0, l = fragments.length; i < l; ++i)
{
var frag = fragments[i].clone();
Kekule.ChemStructureUtils.moveChildBetweenStructFragment(frag, result, frag.getNodes(), frag.getConnectors());
}
return result;
}
},
/**
* Split structFragment with unconnected nodes to multiple ones.
* @param {Kekule.StructureFragment} structFragment
* @returns {Array}
*/
splitStructFragment: function(structFragment)
{
if (!structFragment.hasCtab()) // no ctab, can not split
return [structFragment];
var allNodes = structFragment.getNodes();
if (allNodes.length <= 0)
return [structFragment];
var allConnectors = structFragment.getConnectors();
var splits = [];
var currNodes = [allNodes[0]];
var currConnectors = [];
var currIndex = 0;
//while (currNodes.length < allNodes.length))
do
{
var node = currNodes[currIndex];
var connectors = node.getLinkedConnectors();
if (node.getCrossConnectors)
connectors.concat(node.getCrossConnectors() || []);
Kekule.ArrayUtils.pushUnique(currConnectors, connectors);
for (var i = 0, l = connectors.length; i < l; ++i)
{
var connected = Kekule.ChemStructureUtils._getCascadeConnectedNodesAndConnectors(connectors[i], structFragment);
Kekule.ArrayUtils.pushUnique(currConnectors, connected.connectors);
Kekule.ArrayUtils.pushUnique(currNodes, connected.nodes);
}
++currIndex;
}
while (currIndex < currNodes.length);
splits.push(structFragment);
var restNodes = Kekule.ArrayUtils.exclude(allNodes, currNodes);
var restConnectors = Kekule.ArrayUtils.exclude(allConnectors, currConnectors);
if (restNodes.length > 0)
{
var fragClass = structFragment.getClass();
var newFragment = new fragClass();
Kekule.ChemStructureUtils.moveChildBetweenStructFragment(structFragment, newFragment, restNodes, restConnectors);
var newSplits = Kekule.ChemStructureUtils.splitStructFragment(newFragment);
splits = splits.concat(newSplits);
}
return splits;
},
/**
* Returns a vector reflect coord2 - coord1.
* @param {Kekule.ChemStructureObject} obj1
* @param {Kekule.ChemStructureObject} obj2
* @param {Int} coordMode
* @param {Bool} allowCoordBorrow
* @returns {Hash}
*/
getAbsCoordVectorBetweenObjs: function(obj1, obj2, coordMode, allowCoordBorrow)
{
var coord1 = obj1.getAbsCoordOfMode(coordMode, allowCoordBorrow);
var coord2 = obj2.getAbsCoordOfMode(coordMode, allowCoordBorrow);
return Kekule.CoordUtils.substract(coord2, coord1);
},
/** @private */
_getRef2DCoordOfObj: function(obj, allowCoordBorrow)
{
var coord = obj.getAbsBaseCoord2D? obj.getAbsBaseCoord2D(allowCoordBorrow):
obj.getAbsCoord2D? obj.getAbsCoord2D(allowCoordBorrow): obj.getCoord2D(allowCoordBorrow);
return coord;
},
/** @private */
_getStandardizedLinkedObj2DRelCoords: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings)
{
var result = [];
var baseCoord = Kekule.ChemStructureUtils._getRef2DCoordOfObj(baseObj, allowCoordBorrow);
var linkedObjs = includeUnexposedSiblings? baseObj.getLinkedObjs(): baseObj.getLinkedExposedObjs();
if (includeAttachedMarkers && baseObj.getAttachedMarkers)
{
linkedObjs = linkedObjs.concat(baseObj.getAttachedMarkers() || []);
}
if (excludeObjs && excludeObjs.length)
linkedObjs = AU.exclude(linkedObjs, excludeObjs);
for (var i = 0, l = linkedObjs.length; i < l; ++i)
{
var obj = linkedObjs[i];
var coord = Kekule.ChemStructureUtils._getRef2DCoordOfObj(obj, allowCoordBorrow);
var newCoord = Kekule.CoordUtils.substract(coord, baseCoord);
newCoord = Kekule.CoordUtils.standardize(newCoord);
result.push(newCoord);
}
return result;
},
/**
* Returns an array of connector (and attachedMarkers) angles ( to X-axis ) of object.
* @returns {Array}
* @private
*/
_calcLinkedObj2DAnglesOfObj: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings)
{
var result = [];
var linkedObjCoords = Kekule.ChemStructureUtils._getStandardizedLinkedObj2DRelCoords(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings);
for (var i = 0, l = linkedObjCoords.length; i < l; ++i)
{
var c = linkedObjCoords[i];
if (c.x === 0 && c.y === 0) // zero coord, bypass
continue;
var angle = Math.atan2(c.y, c.x);
if (angle < 0)
angle = Math.PI * 2 + angle;
result.push(angle);
}
result.sort();
return result;
},
/** @private */
_getMostEmptyDirectionOfExistingAngles: function(angles) // angles must be sorted first
{
var l = angles.length;
if (l === 0)
return 0;
else if (l === 1) // only one connector
return -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;
}
}
var result = angles[index] + max / 2;
/* debug
var msg = 'Angles: [';
for (var i = 0; i < l; ++i)
msg += (angles[i] * 180 / Math.PI) + ' '
msg + ']';
console.log(msg, result * 180 / Math.PI);
*/
return result;
}
},
/**
* Get the emptiest 2D direction around obj. Returns angle of that direction.
* @returns {Float}
* @private
*/
getMostEmptyDirection2DAngleOfObj: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings, avoidDirectionAngles)
{
var angles = Kekule.ChemStructureUtils._calcLinkedObj2DAnglesOfObj(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings);
if (avoidDirectionAngles)
{
angles = angles.concat(avoidDirectionAngles);
angles.sort();
}
var result = Kekule.ChemStructureUtils._getMostEmptyDirectionOfExistingAngles(angles);
//console.log('angle', result * 180 / Math.PI, Math.cos(result), Math.sin(result));
return result;
}
};
/**
* An abstract class to analysis and get tokens from text.
* @augments ObjectEx
* @class
* @param {String} text Text to analysis.
*/
Kekule.TokenAnalyzer = Class.create(ObjectEx,
/** @lends Kekule.TokenAnalyzer# */
{
/** @private */
CLASS_NAME: 'Kekule.TokenAnalyzer',
/**
* @constructs
*/
initialize: function($super, text)
{
$super();
this.setSrcText(text);
},
/** @private */
initProperties: function()
{
// private properties
this.defineProp('srcText', {'dataType': DataType.STRING,
'setter': function(value)
{
var v = value || '';
this.setPropStoreFieldValue('srcText', v);
this.setPropStoreFieldValue('srcLength', v.length);
this.setCurrPos(0);
}
});
this.defineProp('srcLength', {'dataType': DataType.INT, 'setter': null, 'serializable': false})
this.defineProp('currPos', {'dataType': DataType.INT, 'serializable': false});
},
/**
* Returns type of char. Neighboring char with same type can be merged into a token.
* Descendants need to override this method.
* @param {String} c
* @returns {Variant}
* @private
*/
getCharType: function(c)
{
// do nothing here
return 0;
},
/**
* Check if two char types are matched and can be merged into a token.
* Descendants may override this method.
* @param {Variant} currT
* @param {Variant} lastT
* @return {Bool}
*/
isCharTypeMatched: function(currT, lastT)
{
return currT === lastT;
},
/** @private */
nextCharInfo: function()
{
var p = this.getCurrPos();
if (p >= this.getSrcLength())
return null;
else
{
var c = this.getSrcText().charAt(p);
this.setCurrPos(p + 1);
return {'char': c, 'charType': this.getCharType(c)};
}
},
/**
* Returns next token of srcText.
* @returns {Hash} {token, tokenType}
* @private
*/
nextTokenInfo: function()
{
var lastCharInfo = this.nextCharInfo();
if (lastCharInfo)
{
var token = lastCharInfo['char']; // .char will cause problem in YUI compressor
var tokenType = lastCharInfo.charType;
var currCharInfo = this.nextCharInfo();
while (currCharInfo && this.isCharTypeMatched(currCharInfo.charType, lastCharInfo.charType))
{
token += currCharInfo['char'];
lastCharInfo = currCharInfo;
currCharInfo = this.nextCharInfo();
}
if (currCharInfo) // now currCharInfo type is different from last, reverse a pos
this.setCurrPos(this.getCurrPos() - 1);
return {'token': token, 'tokenType': tokenType};
}
else
return null;
},
/**
* Returns all token info in src text.
* @returns {Array} Each item is a hash of {token, tokenType}.
*/
getAllTokenInfos: function()
{
var result = [];
var info = this.nextTokenInfo();
while (info)
{
result.push(info);
info = this.nextTokenInfo();
}
return result;
}
});
/**
* Enumeration of chem text token type.
* @enum
*/
Kekule.ChemTextTypes = {
// Chem text char types
/** @private */
CT_ATOM_SYMBOL_LEADING: 1, // highercased alphabet, e.g. 'C' in 'Cu'
/** @private */
CT_ATOM_SYMBOL_FOLLOWING: 2, // lowercased alphabet, e.g. 'u' in 'Cu'
/** @private */
CT_CHARGE_SYMBOL: 3, // '+', '-'
/** @private */
CT_NUMBER: 4,
/** @private */
CT_BRACKET_LEADING: 10, // '(', '[' and '{'
/** @private */
CT_BRACKET_TAILING: 11, // ')', ']' and '}'
/** @private */
CT_SEPARATOR: 20, // space to separate texts
/** @private */
CT_UNKNOWN: 0
};
var CT = Kekule.ChemTextTypes;
/**
* A helper class to analysis chem text (e.g. formula).
* @augments Kekule.TokenAnalyzer
* @class
* @param {String} text Text to analysis.
*/
Kekule.ChemTextAnalyzer = Class.create(Kekule.TokenAnalyzer,
/** @lends Kekule.ChemTextAnalyzer# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemTextAnalyzer',
/** @ignore */
getCharType: function(c)
{
if (c === ' ')
return CT.CT_SEPARATOR;
if (c >= '0' && c <= '9') // number
return CT.CT_NUMBER;
else if (['+', '-'].indexOf(c) >= 0)
return CT.CT_CHARGE_SYMBOL;
else if (['(', '[', '{'].indexOf(c) >= 0)
return CT.CT_BRACKET_LEADING;
else if ([')', ']', '}'].indexOf(c) >= 0)
return CT.CT_BRACKET_TAILING;
else if (c >= 'A' && c <= 'Z')
return CT.CT_ATOM_SYMBOL_LEADING;
else if (c >= 'a' && c <= 'z')
return CT.CT_ATOM_SYMBOL_FOLLOWING;
else
return CT.CT_UNKNOWN;
},
/** @ignore */
isCharTypeMatched: function(currT, lastT)
{
if (Kekule.ArrayUtils.intersect([CT.CT_BRACKET_LEADING, CT.CT_BRACKET_TAILING], [currT, lastT]).length)
return false;
else
return (currT === lastT && lastT !== CT.CT_ATOM_SYMBOL_LEADING) ||
(lastT === CT.CT_ATOM_SYMBOL_LEADING && currT === CT.CT_ATOM_SYMBOL_FOLLOWING);
}
});
/**
* Util class to manipulate molecule formulas.
* @class
*/
Kekule.FormulaUtils = {
/**
* Nestable brackets used to display formula.
* @private
*/
FORMULA_BRACKETS: [['(', ')'], ['[', ']'], ['{', '}']],
/** @private */
FORMULA_BRACKET_TYPE_COUNT: 3,
/**
* Create a formula object from plain text.
* @param {String} text
* @param {Kekule.ChemObject} parent Parent object of formula.
* @param {Kekule.MoleculeFormula} formula If this param is set, changes will be take on this object.
* Otherwise a new instance of formula will be created and returned.
* @returns {Kekule.MoleculeFormula}
*/
textToFormula: function(text, parent, formula)
{
var result = formula || null;
if (result)
{
result.clear();
}
var analyzer = new Kekule.ChemTextAnalyzer(text);
try
{
var tokenInfos = analyzer.getAllTokenInfos();
//console.log(tokenInfos);
if (!result)
result = new Kekule.MolecularFormula(parent);
var tokenLength = tokenInfos.length;
if (tokenLength)
{
var currObj, currSection, lastTokenInfo = null, currCharge;
var currFormula = result;
currSection = {};
var createNewSection = function()
{
currSection = {};
};
var wrapUpCurrSection = function()
{
if (currSection && currSection.obj)
currFormula.appendSection(currSection.obj, currSection.count, currSection.charge);
};
// iterate through tokens
for (var i = 0; i < tokenLength; ++i)
{
var tokenInfo = tokenInfos[i];
var tokenType = tokenInfo.tokenType;
var token = tokenInfo.token;
var handled = false; // mark if currToken is handled
if (tokenType === CT.CT_BRACKET_LEADING) // bracket, new layer
{
wrapUpCurrSection();
var subFormula = new Kekule.MolecularFormula(parent);
subFormula._parentFormula = currFormula;
currFormula = subFormula;
createNewSection();
handled = true;
}
else if (tokenType === CT.CT_BRACKET_TAILING)
{
wrapUpCurrSection();
createNewSection();
currSection.obj = currFormula;
currFormula = currFormula._parentFormula;
tokenInfo.asSymbol = true;
handled = true;
}
else if ([CT.CT_ATOM_SYMBOL_LEADING, CT.CT_ATOM_SYMBOL_FOLLOWING].indexOf(tokenType) >= 0) // atom symbol
{
if (currSection.obj)
{
wrapUpCurrSection();
createNewSection();
}
// TODO: currently only atom can be created
var massNum = currSection.massNum;
if (!massNum) // check if lastToken is unhandled number, if so, it may be the mass num of current symbol
{
if (lastTokenInfo && !lastTokenInfo.handled && (lastTokenInfo.tokenType === CT.CT_NUMBER))
massNum = parseInt(lastTokenInfo.token, 10) || null;
}
var slabel = '' + (massNum || '') + token;
currSection.obj = Kekule.ChemStructureNodeFactory.createByLabel(slabel);
//currSection.obj = new Kekule.Atom(null, token, massNum);
tokenInfo.asSymbol = true;
handled = true;
}
else if (tokenType === CT.CT_CHARGE_SYMBOL)
{
currCharge = (token === '+')? +1: -1;
tokenInfo.asCharge = true;
if (lastTokenInfo.tokenType === CT.CT_NUMBER)
{
var schargeCount;
if (lastTokenInfo.asCount)
{
if (lastTokenInfo.token.length > 1) // last digit should be charge
{
var t = lastTokenInfo.token;
schargeCount = t.substr(t.length - 1);
if (lastTokenInfo.asCount)
currSection.count = parseInt(t.substring(0, t.length - 1), 10);
}
else
{
schargeCount = lastTokenInfo.token;
if (lastTokenInfo.asCount)
currSection.count = 1;
}
}
else
{
schargeCount = lastTokenInfo.token;
lastTokenInfo.asChargeCount = true;
}
currCharge *= parseInt(schargeCount, 10);
}
currSection.charge = currCharge;
handled = true;
}
else if (tokenType === CT.CT_NUMBER)
{
var num = parseInt(token, 10);
if (currCharge && lastTokenInfo.tokenType === CT.CT_CHARGE_SYMBOL && lastTokenInfo.asCharge) // last token is charge
{
currCharge *= num;
currSection.charge = currCharge;
tokenInfo.asChargeCount = true;
handled = true;
}
else if (currSection.obj && lastTokenInfo.asSymbol) // atom symbol already set, number is count
{
currSection.count = num;
tokenInfo.asCount = true;
handled = true;
}
else if (!currSection.obj) // symbol not set, this leading number should be the isotope number
{
currSection.massNum = num;
handled = true;
}
else // do not know the use of number, may be the mass num of next atom symbol
{
}
}
tokenInfo.handled = handled;
lastTokenInfo = tokenInfo;
}
if (currSection && currSection.obj)
wrapUpCurrSection();
}
// debug
//var rt = result.getDisplayRichText();
//console.log(Kekule.Render.RichTextUtils.toText(rt));
return result;
}
finally
{
analyzer.finalize();
}
},
/**
* Returns plain text generated by formula.
* @param {Kekule.MolculeFormula} formula
* @param {Bool} showCharge Whether display formula charge.
* @param {Int} partialChargeDecimalsLength
* @returns {String}
*/
formulaToText: function(formula, showCharge, partialChargeDecimalsLength)
{
/*
var result = '';
var sections = formula.getSections();
*/
/*
var rt = Kekule.Render.ChemDisplayTextUtils.formulaToRichText(formula, true, true);
var result = Kekule.Render.RichTextUtils.toText(rt);
return result;
*/
if (Kekule.ObjUtils.isUnset(showCharge))
showCharge = true;
return FU._convFormulaToText(formula, false, showCharge, partialChargeDecimalsLength);
},
/** @private */
_convFormulaToText: function(formula, showBracket, showCharge, partialChargeDecimalsLength)
{
var result = '';
var sections = formula.getSections();
if (showBracket)
{
var bracketIndex = formula.getMaxNestedLevel() % FU.FORMULA_BRACKET_TYPE_COUNT;
var bracketStart =FU.FORMULA_BRACKETS[bracketIndex][0];
var bracketEnd = FU.FORMULA_BRACKETS[bracketIndex][1];
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 = FU._convFormulaToText(obj, true, false, false, partialChargeDecimalsLength); // do not show charge right after, we will add it later
}
else if (obj.getLabel) // an atom/isotope
{
var subgroup = obj.getLabel();
if (obj.getMassNumber && obj.getMassNumber()) // explicit mass number atom, add a separator before it
subgroup = (result? ' ': '') + subgroup;
}
if (subgroup)
{
var explicitCount = false;
// count
if (sections[i].count != 1)
{
subgroup += sections[i].count;
explicitCount = true;
}
// charge is draw after count
if (showCharge && charge)
{
var chargelabel = FU._convChargeToText(charge, partialChargeDecimalsLength);
//chargelabel += chargeSign;
subgroup += (explicitCount? ' ': '') + chargelabel; // separate count and charge
}
result += subgroup;
}
}
if (showBracket)
result += bracketEnd;
if (showCharge)
{
var charge = formula.getCharge();
if (charge)
{
var chargelabel = FU._convChargeToText(charge, partialChargeDecimalsLength);
result += chargelabel;
}
}
return result;
},
/** @private */
_convChargeToText: function(charge, partialChargeDecimalsLength)
{
if (!charge)
return null;
var chargeSign = (charge > 0)? '+': '-';
var chargeAmount = Math.abs(charge);
var chargelabel = chargeSign;
if (chargeAmount != 1)
{
chargelabel = (partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString()) + chargeSign;
}
return chargelabel;
}
};
var FU = Kekule.FormulaUtils;
// extend MoleculeFormula class
ClassEx.defineProp(Kekule.MolecularFormula, 'text', {
'dataType': DataType.STRING, 'serializable': false,
'getter': function() { return FU.formulaToText(this); },
'setter': function(value)
{
FU.textToFormula(value, this.getParent(), this);
}
});
})();