/**
* @fileoverview
* File for supporting MDL CTAB/MOL/RXN V3000 data.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /core/kekule.elements.js
* requires /core/kekule.electrons.js
* requires /core/kekule.structures.js
* requires /core/kekule.reactions.js
* requires /utils/kekule.textHelper.js
* requires /io/kekule.io.js
* requires /io/kekule.io.mdlBase.js
* requires /localization
*/
/**
* Utility for MDL 3k format.
* @class
* @private
*/
Kekule.IO.Mdl3kUtils = {
/**
* Get start tag of a block.
* @param {String} blockName
* @returns {String}
* @private
*/
get3kBlockStartTag: function(blockName)
{
return 'BEGIN ' + blockName.toUpperCase();
},
/**
* Get end tag of a block.
* @param {String} blockName
* @returns {String}
* @private
*/
get3kBlockEndTag: function(blockName)
{
return 'END ' + blockName.toUpperCase();
},
/**
* Check if value is a atom list rather than single element symbol.
* @param {Object} value
* @private
*/
isAtomList: function(value)
{
var reg = /\[.+\]/;
return value.search(reg) >= 0;
},
/**
* If value is a atom list, analysis its content.
* @param {String} value
* @returns {Hash} {isNot(Bool), atoms(Array)}
* @private
*/
analysisAtomList: function(value)
{
var reg = /\[(.+)\]/g;
//var r = value.match(reg);
var r = reg.exec(value);
if (r && (r.length >= 2))
{
var s = r[1];
r = s.split(',');
var result = {};
result.isAllowList = !(value.trim().indexOf('NOT') == 0);
result.symbols = [];
for (var i = 0, l = r.length; i < l; ++i)
{
result.symbols.push(r[i].trim());
}
return result;
}
else
return null;
},
/**
* Convert a MDL 3000 bond configuration value to {@link Kekule.BondStereo} value.
* @param {Int} value
* @returns {Int} Value from {@link Kekule.BondStereo}
* @private
*/
bondCfgToKekule: function(value)
{
switch (value)
{
case 0: return Kekule.BondStereo.NONE;
case 1: return Kekule.BondStereo.UP;
case 2: return Kekule.BondStereo.UP_OR_DOWN;
case 3: return Kekule.BondStereo.DOWN;
default: return Kekule.BondStereo.NONE;
}
},
/**
* Convert a {@link Kekule.BondStereo} value to MDL 3000 bond configuration value.
* @param {Int} value Value from {@link Kekule.BondStereo}.
* @returns {Int}
* @private
*/
bondStereoToMdlCfg: function(value)
{
switch (value)
{
case Kekule.BondStereo.UP: return 1;
case Kekule.BondStereo.UP_OR_DOWN: return 2;
case Kekule.BondStereo.DOWN: return 3;
//case Kekule.BondStereo.NONE: return 0;
default: return 0;
}
},
/**
* Turn a float coordinate value to a MDL V3000 string.
* @param {Float} value
* @returns {String}
* @private
*/
coordToStr: function(value)
{
return (value || 0).toFixed(6);
}
};
/**
* Utility to analysis a MDL 3000 line and split it into keywords/values.
* @class
* @private
*/
Kekule.IO.Mdl3kValueUtils = {
/** @private */
SEPARATOR: ' ',
/** @private */
KEYVALUE_CONNECTOR: '=',
/** @private */
STR_QUOTE: '"',
/** @private */
ESC_QUOTE: '""',
/** @private */
VALUELIST_PATTERN: /\(.+\)/g,
/**
* Split a string with separator.
* @param {Object} str
* @param {Object} separator If not set, space will be used.
* @returns {Array}
* @private
*/
split: function(str, separator)
{
var sep = separator || Kekule.IO.Mdl3kValueUtils.SEPARATOR;
//return str.split(sep);
var squote = Kekule.IO.Mdl3kValueUtils.STR_QUOTE;
var result = [];
var inQuote = false;
var inBracket = false;
var inParethsis = false;
var curr = '';
for (var i = 0, l = str.length; i < l; ++i)
{
var c = str.charAt(i);
if (c == squote)
{
// check if not a ESC_QUOTE
if (((!inQuote) && (str.charAt(i + 1) == squote))
|| (inQuote && (str.charAt(i - 1) == squote))) // ESC_QUOTE
;
else
inQuote = !inQuote;
}
else if (c == '[')
inBracket = true;
else if (c == ']')
inBracket = false;
else if (c == '(')
inParethsis = true;
else if (c == ')')
inParethsis = false;
if (c == sep)
{
if ((!inQuote) && (!inBracket) && (!inParethsis) && curr)
{
result.push(curr);
curr = '';
}
else
curr += c;
}
else
curr += c;
}
// last one at tail
if (curr)
result.push(curr);
return result;
},
/**
* Merge string array to one string with separator.
* @param {Array} strs
* @param {Object} separator If not set, space will be used.
* @returns {String}
* @private
*/
merge: function(strs, separator)
{
var sep = separator || Kekule.IO.Mdl3kValueUtils.SEPARATOR;
return strs.join(sep);
},
/**
* Check if a str is in key=value format
* @param {String} str
* @returns {Bool}
* @private
*/
isKeyValuePair: function(str)
{
return str.indexOf(Kekule.IO.Mdl3kValueUtils.KEYVALUE_CONNECTOR) >= 0;
},
/**
* Split a key=value pair to {key, value} hash. If '=' not found, regard whole string as value.
* @param {String} str
* @returns {Hash}
* @private
*/
splitKeyValue: function(str)
{
var result = {};
var connector = Kekule.IO.Mdl3kValueUtils.KEYVALUE_CONNECTOR;
var index = str.indexOf(connector);
if (index < 0) // connector not found, regard whole as value
result.value = str;
else // key=value form
{
result.key = str.substr(0, index);
result.value = str.substr(index + connector.length);
}
return result;
},
/**
* Merge value and key to a key=value string. If key not set, return value alone.
* @param {String} key
* @param {String} value
* @returns {String}
* @private
*/
mergeKeyValue: function(value, key)
{
if (!key)
return value;
else
return '' + key + Kekule.IO.Mdl3kValueUtils.KEYVALUE_CONNECTOR + value;
},
/**
* Check if str is a value list surronded by '()'.
* In MDL 3000 CTAB file, multiple values can be grouped in (), for example:
* (5 1 2 3 4 5)
* where first number is the count of values.
* @param {String} str
* @returns {Bool}
* @private
*/
isValueList: function(str)
{
var index = str.search(Kekule.IO.Mdl3kValueUtils.VALUELIST_PATTERN);
return (index >= 0);
},
/**
* Split value list to an array of values
* @param {String} str
* @returns {Array}
* @private
*/
splitValueList: function(str)
{
var reg = /\((.+)\)/;
var r = reg.exec(str);
if (r && (r.length > 1))
{
var s = r[1];
var arr = Kekule.IO.Mdl3kValueUtils.split(s);
arr.shift(); // erase the leading count value
return arr;
}
else
return [str];
},
/**
* Merge strs to a MDL format value list (n val1 val2 ... valn)
* @param {Array} values
* @returns {String}
* @private
*/
mergeValueList: function(values)
{
var s = '(' + values.length;
for (var i = 0, l = values.length; i < l; ++i)
s += Kekule.IO.Mdl3kValueUtils.SEPARATOR + values[i];
s += ')';
return s;
},
/**
* Sometimes MDL 3000 value are surrounded with double quote("), such as "(value)" or "Val""ue". This function will remove the quote.
* @param {String} str
* @returns {String}
* @private
*/
unquoteValue: function(str)
{
var quote = Kekule.IO.Mdl3kValueUtils.STR_QUOTE;
var result = str.trim();
// check if start/end with quote
if (result.substr(0, quote.length) == quote)
result = result.substr(quote.length);
if (result.substr(result.length - quote.length) == quote)
result = result.substr(0, result.length - quote.length);
// then turn inside "" to a single "
var reg = new RegExp(quote + quote, 'g');
result = result.replace(reg, quote);
if (Kekule.IO.Mdl3kValueUtils.isValueList(result))
{
result = Kekule.IO.Mdl3kValueUtils.splitValueList(result);
}
return result;
},
/**
* If str has space, quote it as a single string.
* " is also be turned to "" according to MDL 3000 style.
* @param {String} str
* @returns {String}
* @private
*/
quoteValue: function(str)
{
var quote = Kekule.IO.Mdl3kValueUtils.STR_QUOTE;
var result = '' + str;
//turn inside " to a double ""
var reg = new RegExp(quote, 'g');
result = result.replace(reg, quote + quote);
// then check if space exists, if ture, quote it
if (result.indexOf(Kekule.IO.Mdl3kValueUtils.SEPARATOR) >= 0)
result = quote + result + quote;
return result;
},
/**
* Split a string to a set of legal MDL 3000 values.
* @param {String} str
* @returns {Array} Array of {key, value} hashes.
* @private
*/
splitValues: function(str)
{
var result = [];
var tokens = Kekule.IO.Mdl3kValueUtils.split(str);
for (var i = 0, l = tokens.length; i < l; ++i)
{
var pair = Kekule.IO.Mdl3kValueUtils.splitKeyValue(tokens[i]);
if (pair.value)
pair.value = Kekule.IO.Mdl3kValueUtils.unquoteValue(pair.value);
result.push(pair);
}
return result;
},
/**
* Merge value or {key,value} pair to a MDL 3000 compatible string.
* @param {Array} pairs
* @returns {String}
* @private
*/
mergeValues: function(pairs)
{
var result;
for (var i = 0, l = pairs.length; i < l; ++i)
{
var pair = pairs[i];
var key, value;
if ((typeof(pair) != 'object') || Kekule.ArrayUtils.isArray(pair)) // no key field
value = pair;
else
{
key = pair.key;
value = pair.value;
}
var s;
if (Kekule.ArrayUtils.isArray(value))
s = Kekule.IO.Mdl3kValueUtils.mergeValueList(value);
else
s = Kekule.IO.Mdl3kValueUtils.quoteValue(value);
if (key)
s = Kekule.IO.Mdl3kValueUtils.mergeKeyValue(s, key);
result = Kekule.IO.Mdl3kValueUtils.appendToken(result, s);
}
return result;
},
/**
* Append token to origin string, separate by separator.
* @param {String} origin
* @param {String} token
* @returns {String}
* @private
*/
appendToken: function(origin, token, separator)
{
var sep = separator || Kekule.IO.Mdl3kValueUtils.SEPARATOR;
var s = origin || '';
if ((!s) || (s.substr(s.length - sep.length, sep.length) == sep))
s = token;
else
s += Kekule.IO.Mdl3kValueUtils.SEPARATOR + token;
return s;
}
};
/**
* A special helper class to read / write text data in MDL 3000 files.
* All lines in V3000 file CTAB must begin with "M V30", columns exceed 80 may have "-"
* to continue to next line, this convention is exordinate and handled in this class.
* @class
* @augments Kekule.TextLinesBuffer
* @param {String} text A pack of text to read.
* @private
*/
Kekule.IO.Mdl3kTextBuffer = Class.create(Kekule.TextLinesBuffer,
/** @lends Kekule.Mdl3kTextBuffer# */
{
/** @private */
CLASS_NAME: 'Kekule.Mdl3kTextBuffer',
/** @private */
LEADING_TAG: 'M V30 ',
/** @private */
LINE_CONTI_MARK: '-',
/** @private */
MAX_COL_WIDTH: 80,
/** @constructs */
initialize: function($super, text)
{
$super(text);
},
/** @private */
isStartWithLeadingTag: function(line)
{
return (line.substr(0, this.LEADING_TAG.length) == this.LEADING_TAG);
},
/** @private */
isEndWithContinueMark: function(line)
{
var s = line.trim();
return (s.substr(s.length - this.LINE_CONTI_MARK.length) == this.LINE_CONTI_MARK);
},
/** @private */
getLineWithout3kTag: function(line)
{
var l = this.LEADING_TAG.length;
if (line.substr(0, l) == this.LEADING_TAG)
return line.substr(l);
else
return line;
},
/**
* Get value of line in index. 'M V30' tag will be removed from head of line.
* If the line end with '-', next line is its continuous and will appended to the end of this line.
* @param {Int} index
* @returns {Hash} {line, lineCount} If line continued with other line, lineCount will report total lines actually read.
*/
getLineAtEx: function(index)
{
var line = this.getLineAt(index);
var count = 1;
// check end mark
if (line && this.isEndWithContinueMark(line) && this.isStartWithLeadingTag(this.getLines()[index]))
{
// remove continue mark
var p = line.lastIndexOf(this.LINE_CONTI_MARK);
line = line.substr(0, p);
var nextResult = this.getLineAtEx(index + 1);
line += nextResult.line;
count += nextResult.lineCount;
}
var result = {'line': line, 'lineCount': count};
return result;
},
/**
* Get value of line in index. 'M V30' tag will be removed from head of line.
* Note the continue mark is not handled in this function.
* @param {Int} index
* @returns {String}
*/
getLineAt: function($super, index)
{
var line = $super(index);
return this.getLineWithout3kTag(line);
},
/**
* Get value of current line and move currLineNo to next line.
* @returns {String}
*/
readLine: function()
{
var r = this.getLineAtEx(this.getCurrLineNo());
this.incCurrLineNo(r.lineCount);
return r.line;
},
/**
* Insert a line at current position and move currLineNo to next line.
* @param {String} line
* @param {Bool} not3kMode If in 3k mode, each line will automatically start with M V30
* and line longer than 80 cols will be wrapped
*/
writeLine: function($super, line, not3kMode)
{
if (!not3kMode)
{
var s = line;
// add leading tag
if (!this.isStartWithLeadingTag(line))
s = this.LEADING_TAG + s;
// wrap lines
var sexceed;
if (s.length > this.MAX_COL_WIDTH)
{
var cmark = this.LINE_CONTI_MARK;
var sep = Kekule.IO.Mdl3kValueUtils.SEPARATOR;
var firstLineWidth = this.MAX_COL_WIDTH - cmark.length/* - sep.length*/;
sexceed = s.substr(firstLineWidth);
s = s.substr(0, firstLineWidth);
// add continue mark
s += /*sep + */cmark;
}
var r = $super(s);
if (sexceed)
r = this.writeLine(sexceed, not3kMode);
return r;
}
else
return $super(line);
},
/**
* Insert array of lines at current position and move currLineNo.
* @param {Array} lines
*/
writeLines: function($super, lines, not3kMode)
{
for (var i = 0, l = lines.length; i < l; ++i)
this.writeLine(lines[i], not3kMode);
},
/**
* Read lines to extract a whole block and returns a new buffer to it.
* @param {String} blockName
* @param {String} reservePos
* @returns {Kekule.IO.Mdl3kTextBuffer}
*/
getBlockBuffer: function(blockName, reservePos)
{
var result;
var startTag = Kekule.IO.Mdl3kUtils.get3kBlockStartTag(blockName);
var endTag = Kekule.IO.Mdl3kUtils.get3kBlockEndTag(blockName);
var oldPos = this.getCurrLineNo();
this.reset();
var line = this.readLine().trim();
while ((line != startTag) && (!this.eof()))
{
line = this.readLine().trim();
}
if (this.eof()) // not found
result = null;
else // find end tag
{
var startPos = this.getCurrLineNo();
endPos = this.getCurrLineNo();
line = this.readLine().trim();
while ((line != endTag) && (!this.eof()))
{
endPos = this.getCurrLineNo();
line = this.readLine().trim();
}
if (line == endTag)
var lines = this.getLines().slice(startPos, endPos);
else // if eof, the block is to the end of data
var lines = this.getLines().slice(startPos);
result = new Kekule.IO.Mdl3kTextBuffer(lines);
}
// reverse currLineNo
if (reservePos)
this.setCurrLineNo(oldPos);
return result;
}
});
/**
* Base class of readers to read different MDL 3000 blocks.
* @class
* @augments Kekule.IO.MdlBlockReader
* @private
*/
Kekule.IO.Mdl3kBlockReader = Class.create(Kekule.IO.MdlBlockReader,
/** @lends Kekule.IO.Mdl3kBlockReader# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Mdl3kBlockReader',
/** @private */
createTextBuffer: function()
{
return (new Kekule.IO.Mdl3kTextBuffer());
},
/** @private */
doReadBlock: function(textBuffer, parentObj)
{
var buffer;
var newBuffer = !(textBuffer instanceof Kekule.IO.Mdl3kTextBuffer);
if (newBuffer) // need to wrap
{
buffer = new Kekule.IO.Mdl3kTextBuffer();
buffer.setLines(textBuffer.getUnreadLines());
buffer.reset();
}
else
buffer = textBuffer;
var startLineNo = buffer.getCurrLineNo();
var result = this.doRead3kBlock(buffer, parentObj);
var endLineNo = buffer.getCurrLineNo();
if (newBuffer) // move old textBuffer line cusor to corresponding position
{
var readLineCount = endLineNo - startLineNo;
textBuffer.incCurrLineNo(readLineCount);
}
return result;
},
/** @private */
doRead3kBlock: function(textBuffer, parentObj)
{
// do nothing here
}
});
/**
* Base class of readers to read different MDL 3000 blocks.
* @class
* @augments Kekule.IO.MdlBlockWriter
* @private
*/
Kekule.IO.Mdl3kBlockWriter = Class.create(Kekule.IO.MdlBlockWriter,
/** @lends Kekule.IO.Mdl3kBlockWriter# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Mdl3kBlockWriter',
/** @private */
createTextBuffer: function()
{
return (new Kekule.IO.Mdl3kTextBuffer());
},
/** @private */
writeStartTag: function(blockName, textBuffer)
{
textBuffer.writeLine(Kekule.IO.Mdl3kUtils.get3kBlockStartTag(blockName));
},
/** @private */
writeEndTag: function(blockName, textBuffer)
{
textBuffer.writeLine(Kekule.IO.Mdl3kUtils.get3kBlockEndTag(blockName));
},
/** @private */
doWriteBlock: function(obj, textBuffer)
{
var buffer;
var newBuffer = !(textBuffer instanceof Kekule.IO.Mdl3kTextBuffer);
if (newBuffer) // need to wrap
{
buffer = new Kekule.IO.Mdl3kTextBuffer();
buffer.setLines(textBuffer.getUnreadLines());
buffer.reset();
}
else
buffer = textBuffer;
var result = this.doWrite3kBlock(obj, buffer);
if (newBuffer)
{
textBuffer.appendLines(newBuffer.getLines());
}
return result;
},
/** @private */
doWrite3kBlock: function(obj, textBuffer)
{
// do nothing here
}
});
/**
* Class to read and anaylsis MDL 3000 Connection Table block.
* @class
* @augments Kekule.IO.Mdl3kBlockReader
* @private
*/
Kekule.IO.Mdl3kCTabReader = Class.create(Kekule.IO.Mdl3kBlockReader,
/** @lends Kekule.IO.Mdl3kCTabReader# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Mdl3kCTabReader',
/** @private */
doRead3kBlock: function(textBuffer, parentObj)
{
// get Ctab block from textBuffer, if begin tag not found, regard the block start from the first line
var buffer = textBuffer.getBlockBuffer('CTAB');
if (!buffer)
buffer = textBuffer;
return this.analysisCTab(buffer);
},
/**
* Analysis the whole ctab
* @param {Kekule.TextLinesBuffer} textBuffer
* @private
*/
analysisCTab: function(textBuffer)
{
var result = {};
// count line
var line = textBuffer.readLine();
var countInfo = this.analysisCountLine(line);
if (!countInfo) // error
return null;
// atom block
var atomIndexMap = [];
var atomInfo;
var atomBuffer = textBuffer.getBlockBuffer('ATOM');
if (atomBuffer)
atomInfo = this.analysisAtomBlock(atomBuffer, atomIndexMap);
else // atom block not found, illegal
{
Kekule.raise(/*Kekule.ErrorMsg.MDL3000_ATOMBLOCK_NOT_FOUND*/Kekule.$L('ErrorMsg.MDL3000_ATOMBLOCK_NOT_FOUND'));
return null;
}
// bond block
var bondInfo;
var bondBuffer = textBuffer.getBlockBuffer('BOND');
if (bondBuffer)
bondInfo = this.analysisBondBlock(bondBuffer, atomIndexMap);
// link atom line, ignore currently
// Sgroup block
var sgroupInfo;
var sgroupBuffer = textBuffer.getBlockBuffer('SGROUP');
if (sgroupBuffer)
sgroupInfo = this.analysisSGroupBlock(sgroupBuffer, atomIndexMap);
// other blocks are currently ignored
result.countInfo = countInfo;
result.atomInfos = atomInfo;
result.atomIndexMap = atomIndexMap;
if (bondInfo)
result.bondInfos = bondInfo;
if (sgroupInfo)
result.sgInfos = sgroupInfo;
return result;
},
/**
* Read count line of CTAB.
* @private
*/
analysisCountLine: function(line)
{
var result = {};
var values = Kekule.IO.Mdl3kValueUtils.splitValues(line);
// format: M V30 COUNTS na nb nsg n3d chiral [REGNO=regno]
var tag = values.shift().value;
// COUNTS tag, assert the count line format is correct
if (tag != 'COUNTS')
{
Kekule.raise(/*Kekule.ErrorMsg.MALFORMED_MDL3000_COUNTLINE*/Kekule.$L('ErrorMsg.MALFORMED_MDL3000_COUNTLINE'));
return;
}
// na: number of atoms
var s = values.shift().value;
result.atomCount = parseInt(s, 10);
// nb: number of bonds
s = values.shift().value;
result.bondCount = parseInt(s, 10);
// nsg: Sgroup count. This info does not exist in MDL 2000 CTAB
s = values.shift().value;
result.sgroupCount = parseInt(s, 10);
// chiral
s = values.shift().value;
result.isChiral = (parseInt(s, 10) != 0);
// regno, ignore
return result;
},
/** @private */
analysisAtomBlock: function(textBuffer, atomIndexMap)
{
var coordMode = Kekule.CoordMode.COORD2D;
var atomInfos = [];
//atomInfos.isCoord3D = false;
//for (var i = 0; i < countInfo.atomCount; ++i)
while (!textBuffer.eof())
{
line = textBuffer.readLine();
var atomInfo = this.analysisAtomLine(line);
var actualIndex = atomInfos.push(atomInfo) - 1;
// as index of atom in MDL file may be disordered, here use a map to keep
// the relation between array index and MDL atom index
atomIndexMap[atomInfo.index] = actualIndex;
// check coord mode
if (atomInfo.z) // has z value
coordMode = Kekule.CoordMode.COORD3D;
}
atomInfos.coordMode = coordMode;
atomInfos.isCoord3D = (coordMode == Kekule.CoordMode.COORD3D);
return atomInfos;
},
/** @private */
analysisAtomLine: function(line, atomIndexMap)
{
var result = {};
// format: M V30 index type x y z aamap [optional key/values]
var values = Kekule.IO.Mdl3kValueUtils.splitValues(line);
// index
result.index = parseInt(values.shift().value, 10);
// type
var s = values.shift().value.trim();
if (Kekule.IO.Mdl3kUtils.isAtomList(s))
{
var atomListInfo = Kekule.IO.Mdl3kUtils.analysisAtomList(s);
if (atomListInfo)
{
result.atomListInfo = atomListInfo;
result.symbol = 'L';
}
}
else
result.symbol = s;
// x y z
result.x = parseFloat(values.shift().value);
result.y = parseFloat(values.shift().value);
result.z = parseFloat(values.shift().value);
/*
s = values.shift().value;
if (s === '0') // 0, not 0.0000000, means no z coordinate
result.coordMode = Kekule.CoordMode.COORD2D;
else
{
result.z = parseFloat(values.shift().value);
result.coordMode = Kekule.CoordMode3D;
}
*/
// aamap: Atom-atom mapping, for reaction, ignored currently
values.shift();
// the rest if optional values in key=value form
for (var i = 0, l = values.length; i < l; ++i)
{
var pair = values[i];
var key = pair.key;
var value = pair.value;
switch (key)
{
case 'CHG': // Atom charge
{
var c = parseInt(value, 10);
if (c)
result.charge = c;
break;
}
case 'RAD': // radical
{
var c = parseInt(value, 10);
if (c)
result.radical = c;
break;
}
case 'CFG': // Stereo configuration
{
var c = parseInt(value, 10);
if (c)
result.parity = c;
break;
}
case 'MASS': // isotope mass
{
result.massNumber = parseFloat(value);
break;
}
case 'VAL': // Valence
{
// TODO: ignored currently
break;
}
case 'HCOUNT': // Query hydrogen count
{
var c = parseInt(value, 10);
if (c)
result.hydrongenCount = (c == -1)? 0: c - 1;
break;
}
case 'RGROUPS': // nvals is the number of Rgroups that comprise this R# atom.
{
var c = parseInt(value, 10);
if (c)
result.rgroupCount = c;
break;
}
/*
case 'STBOX': // Stereo box
case 'INVRET': // Configuration inversion, for reaction
case 'EXACHG': // Exact change, for reaction
case 'SUBST': // Query substitution count
case 'UNSAT': // Query unsaturation flag
case 'RBCNT': // Query ring bond count
case 'ATTCHPT': // Rgroup member attachment points
case 'ATTCHORD': //
case 'CLASS': // provides the class information for a collapsed template atom
case 'SEQID': // supports a positive integer value to capture residue sequence id information for a template
{
// ignored
break;
}
*/
}
}
return result;
},
/** @private */
analysisBondBlock: function(textBuffer, atomIndexMap)
{
var bondInfos = [];
//for (var i = 0; i < countInfo.bondCount; ++i)
while (!textBuffer.eof())
{
line = textBuffer.readLine();
var bondInfo = this.analysisBondLine(line, atomIndexMap);
bondInfos[bondInfo.index] = bondInfo;
}
return bondInfos;
},
/** @private */
analysisBondLine: function(line, atomIndexMap)
{
var result = {};
// format: M V30 index type atom1 atom2 [optional key/values]
var values = Kekule.IO.Mdl3kValueUtils.splitValues(line);
// index
result.index = parseInt(values.shift().value, 10);
// type (bond order)
var s = values.shift().value;
result.order = Kekule.IO.MdlUtils.bondTypeToKekuleOrder(parseInt(s, 10));
// atom1 atom2: atom indexes
result.atomIndex1 = atomIndexMap[parseInt(values.shift().value, 10)];
result.atomIndex2 = atomIndexMap[parseInt(values.shift().value, 10)];
// optional key/values
for (var i = 0, l = values.length; i < l; ++i)
{
var key = values[i].key;
var value = values[i].value;
switch (key)
{
case 'CFG': // Bond configuration
{
var c = parseInt(value, 10);
result.stereo = Kekule.IO.Mdl3kUtils.bondCfgToKekule(c);
break;
}
case 'ENDPTS': // multiple endpoint,
// One-to-all for organometallics or one-to-any for generics. For example,
// ENDPTS=(5 1 2 3 4 5) ATTACH=ALL
{
if (Kekule.ArrayUtils.isArray(value)) // list value
{
result.endAtomIndexes = [];
for (var j = 0, k = value.length; j < k; ++j)
{
result.endAtomIndexes.push(atomIndexMap[parseInt(value[j], 10)]);
}
}
else // number value?
result.endAtomIndexes = [atomIndexMap[parseInt(value, 10)]];
}
case 'TOPO': // topological query property, chain or ring, ignore
case 'RXCTR': // Reacting center status, ignore
case 'STBOX': // Stereo box, ignore
{
break;
}
}
}
return result;
},
/** @private */
analysisSGroupBlock: function(textBuffer, atomIndexMap)
{
var sgroupInfos = [];
var defValues = null;
// check if the first line is default value
// [M V30 DEFAULT [CLASS=class] -]
var pos = textBuffer.getCurrLineNo();
line = textBuffer.readLine();
var values = Kekule.IO.Mdl3kValueUtils.splitValues(line);
if (values.shift().value == 'DEFAULT') // default line, fetch all default values
defValues = this.fetchSGroupOptionalValues(line, atomIndexMap);
else // not default line, turn textBuffer pos back
textBuffer.setCurrLineNo(pos);
// iterate
while (!textBuffer.eof())
{
line = textBuffer.readLine();
var sgInfo = this.analysisSGroupLine(line, atomIndexMap, defValues);
sgroupInfos[sgInfo.index] = sgInfo;
}
return sgroupInfos;
},
/** @private */
analysisSGroupLine: function(line, atomIndexMap, defValues)
{
var result = {};
var values = Kekule.IO.Mdl3kValueUtils.splitValues(line);
// format:
// M V30 index type extindex [optional]
// index
result.index = parseInt(values.shift().value, 10);
// type
result.sgType = values.shift().value.trim();
// extIndex
result.extIndex = parseInt(values.shift().value, 10); // no use currently
// optional values
var opValues = this.fetchSGroupOptionalValues(values, atomIndexMap);
// merge with defValues
if (defValues)
opValues = Object.extend(defValues, opValues);
result = Object.extend(result, opValues);
return result;
},
/** @private */
fetchSGroupOptionalValues: function(pairs, atomIndexMap)
{
var result = {};
for (var i = 0, l = pairs.length; i < l; ++i)
{
var key = pairs[i].key;
var value = pairs[i].value;
switch (key)
{
case 'ATOMS': // atom indexes that define the Sgroup
{
result.atomIndexes = [];
for (var j = 0, k = value.length; j < k; j++)
result.atomIndexes.push(atomIndexMap[value[j]]);
break;
}
case 'XBONDS': // indexes of xbonds
{
result.crossBondIndexes = [].concat(value);
break;
}
case 'SUBTYPE': // Sgroup subtype, not used now
{
result.subType = value;
break;
}
case 'LABEL': // display label for sgroup
{
result.label = value;
break;
}
case 'CSTATE':
// xbond is the crossing bond of the expanded abbreviation Sgroup
// cbvx - cvbz is the vector to contracted abbreviation Sgroup
{
if (!result.bondVectors)
result.bondVectors = [];
// first one is bond index
var bondIndex = parseInt(value.shift(), 10);
var x = parseFloat(value.shift()) || 0;
var y = parseFloat(value.shift()) || 0;
var z = parseFloat(value.shift()) || 0;
result.bondVectors[bondIndex] = {
'bondIndex': bondIndex,
'x': x, 'y': y, 'z': z
};
break;
}
case 'CLASS': // character string for abbreviation Sgroup class
{
result.sgClass = value;
break;
}
/*
case 'CBONDS': // Only used for data Sgroups, ignore
case 'PATOMS': // atom indexes of an atom in the paradigmatic repeating unit for a multiple group, ignore
case 'MULT': // multiple group multiplier, ignore
case 'CONNECT': // connectivity, EU, HH, or HT, ignore
case 'PARENT': // parent Sgroup index, ignore
case 'COMPNO': // component order number, ignore
case 'XBHEAD': // crossing bonds that cross the ��head�� bracket, ignore
case 'XBCORR': // pairs of crossing-bond correspondence, ignore
case 'BRKXYZ': // double (X,Y,Z) display coordinates in each bracket, ignore
case 'ESTATE': // expanded display state information for abbreviation Sgroups, ignore
case 'FIELDNAME': // the name of data field for Data Sgroup, ignore
case 'FIELDDISP': // Data Sgroup field display information, ignore
case 'FIELDDATA': // query or field data, ignore
case 'FIELDINFO': // program-specific field information, ignore
case 'QUERYTYPE': // type of query or no query if missing, ignore
case 'QUERYOP': // query operator, ignore
case 'BRKTYP': // displayed bracket style, ignore
case 'SEQID': // a positive integer value to capture residue sequence id information, ignore
case 'SAP': // ignore
{
break;
}
*/
}
}
return result;
}
});
/**
* Class to write MDL 3000 Connection Table block.
* @class
* @augments Kekule.IO.Mdl3kBlockWriter
* @private
*/
Kekule.IO.Mdl3kCTabWriter = Class.create(Kekule.IO.Mdl3kBlockWriter,
/** @lends Kekule.IO.Mdl3kCTabWriter# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Mdl3kCTabWriter',
/** @constructs */
initialize: function($super, coordMode)
{
$super();
this.setCoordMode(coordMode || Kekule.CoordMode.UNKNOWN);
},
/** @private */
initProperties: function()
{
this.defineProp('coordMode', {'dataType': DataType.INT, 'deaultValue': Kekule.CoordMode.UNKNOWN});
// Whether write a 2k style countline before Ctab block for compatibility
// this.defineProp('writeCompatibilityCountline', {'dataType': DataType.BOOL});
},
/** @private */
doWrite3kBlock: function(obj, textBuffer)
{
Kekule.IO.MdlUtils.assertIlegalForCtabOutput(obj);
return this.outputCtab(obj, textBuffer);
},
/**
* Output atoms and bonds in molecule to text data.
* @param {Kekule.StructureFragment} mol
* @param {Kekule.IO.Mdl3kTextBuffer} textBuffer
* @private
*/
outputCtab: function(mol, textBuffer)
{
var molInfo = Kekule.IO.MdlStructureUtils.getMoleculeCtabStructureInfo(mol);
/*
if (this.getWriteCompatibilityCountline())
textBuffer.writeLine(this.generateCompatibilityCountLine(molInfo));
*/
// write start tag
this.writeStartTag('CTAB', textBuffer);
// decide coordMode
if (this.getCoordMode() != Kekule.CoordMode.UNKNOWN)
molInfo.coordMode = this.getCoordMode();
// count line
textBuffer.writeLine(this.generateCountLine(molInfo));
// atom block
this.outputAtomBlock(mol, molInfo, textBuffer);
// bond block
this.outputBondBlock(mol, molInfo, textBuffer);
// Sgroup block
this.outputSgroupBlock(mol, molInfo, textBuffer);
// write end tag
this.writeEndTag('CTAB', textBuffer);
},
/**
* Generate 2k style count line string of molecule for compatibility.
* @param {Hash} molInfo Info returned by {@link Kekule.IO.MdlStructureUtils.getMoleculeCtabStructureInfo}.
* @returns {String}
* @private
*/
generateCompatibilityCountLine: function(molInfo)
{
return Kekule.IO.MdlStructureUtils.generateClassicStyleCountLine(molInfo, Kekule.IO.MdlVersion.V3000);
},
/**
* Generate count line string of molecule.
* @param {Hash} molInfo Info returned by {@link Kekule.IO.MdlStructureUtils.getMoleculeCtabStructureInfo}.
* @returns {String}
* @private
*/
generateCountLine: function(molInfo)
{
// format: M V30 COUNTS na nb nsg n3d chiral [REGNO=regno]
var values = ['COUNTS']; // tag
values.push(molInfo.atoms.length); // na: atom count
values.push(molInfo.bonds.length); // nb: bond count
values.push(molInfo.subGroups.length); // sg: sgroup count
values.push(0); // n3d, number of 3D constraints, ignored
values.push(0); //TODO: chiral: chiral mark, ignored currently
return Kekule.IO.Mdl3kValueUtils.mergeValues(values);
},
/**
* Output atom block to textBuffer.
* @private
*/
outputAtomBlock: function(mol, molInfo, textBuffer)
{
this.writeStartTag('ATOM', textBuffer);
for (var i = 0, l = molInfo.atoms.length; i < l; ++i)
{
var atom = molInfo.atoms[i];
var line = this.generateAtomLine(i, molInfo.coordMode, atom);
textBuffer.writeLine(line);
}
this.writeEndTag('ATOM', textBuffer);
},
/**
* Generate a line about atom in atom block.
* @private
*/
generateAtomLine: function(index, coordMode, atom)
{
// Coordinate convert function
var cf = Kekule.IO.Mdl3kUtils.coordToStr;
// format: M V30 index type x y z aamap [optional key/values]
var values = [index + 1, Kekule.IO.MdlStructureUtils.getAtomTypeStr(atom)];
// coord
if (coordMode == Kekule.CoordMode.COORD2D)
{
var coord = atom.getAbsCoord2D();
values = values.concat([cf(coord.x), cf(coord.y), cf(0)]);
}
else
{
var coord = atom.getAbsCoord3D();
values = values.concat([cf(coord.x), cf(coord.y), cf(coord.z)]);
}
// aamap, ignore
values.push(0);
// additional properties
if (atom.getCharge && atom.getCharge())
values.push({'key': 'CHG', 'value': atom.getCharge()});
if (atom.getRadical && atom.getRadical())
values.push({'key': 'RAD', 'value': atom.getRadical()});
if (atom.getParity && atom.getParity())
values.push({'key': 'CFG', 'value': atom.getParity()});
if (atom.getMassNumber && atom.getMassNumber())
values.push({'key': 'MASS', 'value': atom.getMassNumber()});
if (atom.getExplicitHydrogenCount && (!Kekule.ObjUtils.isUnset(atom.getExplicitHydrogenCount())))
values.push({'key': 'HCOUNT', 'value': atom.getExplicitHydrogenCount() + 1});
return Kekule.IO.Mdl3kValueUtils.mergeValues(values);
},
/**
* Output bond block to textBuffer.
* @private
*/
outputBondBlock: function(mol, molInfo, textBuffer)
{
if (molInfo.bonds.length <= 0)
return;
this.writeStartTag('BOND', textBuffer);
for (var i = 0, l = molInfo.bonds.length; i < l; ++i)
{
var bond = molInfo.bonds[i];
var line = this.generateBondLine(i, bond, molInfo.atoms);
textBuffer.writeLine(line);
}
this.writeEndTag('BOND', textBuffer);
},
/**
* Generate a line about bond in bond block.
* @private
*/
generateBondLine: function(index, bond, atomList)
{
// format: M V30 index type atom1 atom2 [optional key/values]
var values = [index + 1,
Kekule.IO.MdlUtils.kekuleBondOrderToMdlType(
bond.getBondOrder? bond.getBondOrder(): Kekule.BondOrder.UNSET
)];
// atom1 atom2
var count = 0;
var atoms = [];
var endPoints = [];
var nodeGroup = Kekule.IO.MdlStructureUtils.splitConnectedNodes(bond);
atoms = [atomList.indexOf(nodeGroup.primaryNodes[0]) + 1, atomList.indexOf(nodeGroup.primaryNodes[1]) + 1];
// if indexOf not found, it will returns -1, plus 1 just got zero
if (nodeGroup.remainNodes && nodeGroup.remainNodes.length)
{
for (var i = 0, l = nodeGroup.remainNodes.length; i < l; ++i)
endPoints.push(atomList.indexOf(nodeGroup.remainNodes[i]) + 1);
}
values = values.concat(atoms);
// optional values
if (bond.getStereo && bond.getStereo())
values.push({'key': 'CFG', 'value': Kekule.IO.Mdl3kUtils.bondStereoToMdlCfg(bond.getStereo())});
if (endPoints.length > 0)
{
values.push({'key': 'ENDPTS', 'value': endPoints});
values.push({'key': 'ATTACH', 'value': 'ALL'}); // currrent assume connectivity is all
}
return Kekule.IO.Mdl3kValueUtils.mergeValues(values);
},
/**
* Output Sgroup block to textBuffer.
* @private
*/
outputSgroupBlock: function(mol, molInfo, textBuffer)
{
if (molInfo.subGroups.length <= 0)
return;
this.writeStartTag('SGROUP', textBuffer);
// here we handle sub-group (super-atom) only.
for (var i = 0, l = molInfo.subGroups.length; i < l; ++i)
{
var line = this.generateSgroupLine(i, molInfo.subGroups[i], molInfo);
textBuffer.writeLine(line);
}
this.writeEndTag('SGROUP', textBuffer);
},
/**
* Generate a line about sub-group in Sgroup block.
* @private
*/
generateSgroupLine: function(index, subGroup, molInfo)
{
var atomList = molInfo.atoms;
var bondList = molInfo.bonds;
// format: // M V30 index type extindex [optional]
var values = [index + 1, 'SUP', index + 1];
// optional values
// ATOMS: atom indexes that define the Sgroup
var atomIndexes = [];
var atoms = subGroup.getLeafNodes();
for (var i = 0, l = atoms.length; i < l; ++i)
{
var index = atomList.indexOf(atoms[i]);
if (index >= 0)
atomIndexes.push(index + 1);
}
atomIndexes = atomIndexes.sort();
values.push({'key': 'ATOMS', 'value': atomIndexes});
// LABEL: display label
var slabel = (subGroup.getAbbr && subGroup.getAbbr())
|| (subGroup.getFormulaText && subGroup.getFormulaText())
|| (subGroup.getName && subGroup.getName());
if (slabel)
values.push({'key': 'LABEL', 'value': slabel});
// XBONDS: indexes of xbonds
// CSTATE: xbond vector
var xbonds = subGroup.getCrossConnectors();
var xbondIndexes = [];
var xbondVectors = [];
var cf = Kekule.IO.Mdl3kUtils.coordToStr;
for (var i = 0, l = xbonds.length; i < l; ++i)
{
var index = bondList.indexOf(xbonds[i]);
if (index >= 0)
{
xbondIndexes.push(index + 1);
// vector: bondIndex, x,y z
var vectorArray = [index + 1]; // index
// vector x/y/z calculation
var nodeGroup = Kekule.IO.MdlStructureUtils.splitConnectedNodes(xbonds[i]);
var atoms = nodeGroup.primaryNodes;
var vector = {};
if (atoms.length == 2)
{
if (molInfo.coordMode == Kekule.CoordMode.COORD2D)
{
vector = Kekule.CoordUtils.substract(atoms[0].getAbsCoord2D(), atoms[1].getAbsCoord2D());
vectorArray = vectorArray.concat([cf(vector.x), cf(vector.y), 0]);
}
else
{
vector = Kekule.CoordUtils.substract(atoms[0].getAbsCoord3D(), atoms[1].getAbsCoord3D());
vectorArray = vectorArray.concat([cf(vector.x), cf(vector.y), cf(vector.z)]);
}
}
xbondVectors.push(vectorArray);
}
}
xbondIndexes = xbondIndexes.sort();
values.push({'key': 'XBONDS', 'value': xbondIndexes});
for (var i = 0, l = xbondVectors.length; i < l; ++i)
values.push({'key': 'CSTATE', 'value': xbondVectors[i]});
return Kekule.IO.Mdl3kValueUtils.mergeValues(values);
}
});