kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,563 lines (1,507 loc) • 109 kB
JavaScript
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /utils/kekule.textHelper.js
* requires /core/kekule.common.js
* requires /spectrum/kekule.spectrum.core.js
* requires /io/kekule.io.js
* requires /localization
*/
(function(){
"use strict";
var AU = Kekule.ArrayUtils;
/*
* Default options to read/write JCAMP format data.
* @object
*/
Kekule.globalOptions.add('IO.jcamp', {
enableXYDataValueCheck: true,
dataValueCheckAllowedErrorRatio: 0.001, // allow 0.1% error of X/Y value check
maxCharsPerLine: 80, // theoretically, there should be no more than 80 chars per line in JCAMP file
});
/**
* Namespace of JCAMP IO.
* @namespace
*/
Kekule.IO.Jcamp = {};
var Jcamp = Kekule.IO.Jcamp;
// Some consts related with JCAMP
/** @ignore */
Kekule.IO.Jcamp.Consts = {
DATA_LABEL_FLAG: '##',
DATA_LABEL_TERMINATOR: '=',
PRIVATE_LABEL_PREFIX: '$',
SPECIFIC_LABEL_PREFIX: '.',
LABEL_BLOCK_BEGIN: 'TITLE',
LABEL_BLOCK_END: 'END',
LABEL_DATA_TYPE: 'DATATYPE',
//LABEL_DATA_CLASS: 'DATACLASS',
LABEL_BLOCK_COUNT: 'BLOCKS',
LABEL_BLOCK_ID: 'BLOCKID',
LABEL_CROSS_REF: 'CROSSREFERENCE',
INLINE_COMMENT_FLAG: '$$',
TABLE_LINE_CONTI_MARK: '=',
UNKNOWN_VALUE: NaN, // special value indicating an unknown variable value in data table
//LABEL_DX_VERSION: 'JCAMP-DX',
LABEL_DX_VERSION: 'JCAMPDX',
//LABEL_CS_VERSION: 'JCAMP-CS',
LABEL_CS_VERSION: 'JCAMPCS',
DATA_FORMAT_GROUP_LEADING: '(',
DATA_FORMAT_GROUP_TAILING: ')',
DATA_FORMAT_LOOP: '..',
DATA_FORMAT_INC: '++',
DATA_FORMAT_SYMBOL_ASSIGNMENT: 'A',
DATA_FORMAT_PLOT_DESCRIPTOR_DELIMITER: ',',
SIMPLE_VALUE_DELIMITER: ',',
CROSS_REF_TYPE_TERMINATOR: ':', // to delimiter a cross reference text, e.g. 'STRUCTURE: BLOCK_ID= 1', 'NMR PEAK ASSIGNMENTS: BLOCK_ID= 2'
DATA_VARLIST_FORMAT_XYDATA: 1,
DATA_VARLIST_FORMAT_XYPOINTS: 2,
DATA_VARLIST_FORMAT_XYWPOINTS: 2, // (XYW...XYW) for peak table, now we can handle this same as XYPOINTS
DATA_VARLIST_FORMAT_VAR_GROUPS: 5,
GROUPED_VALUE_GROUP_DELIMITER: '\n',
GROUPED_VALUE_GROUP_DELIMITER_PATTERN: /[;\s+]/g,
GROUPED_VALUE_ITEM_DELIMITER: ',',
GROUPED_VALUE_ITEM_DELIMITER_PATTERN: /,/g,
GROUPED_VALUE_STR_ENCLOSER_LEADING: '<',
GROUPED_VALUE_STR_ENCLOSER_TAILING: '>',
GROUPED_VALUE_EXPLICIT_GROUP_LEADING: '(',
GROUPED_VALUE_EXPLICIT_GROUP_TAILING: ')',
NTUPLE_DEFINITION_ITEM_DELIMITER: ',\t',
VALUE_STR_EXPLICIT_QUOTE: '"',
VALUE_ABNORMAL_NUM: '?',
MOL_FORMULA_SUP_PREFIX: '^',
MOL_FORMULA_SUB_PREFIX: '/',
};
var JcampConsts = Kekule.IO.Jcamp.Consts;
/**
* Enumeration of JCAMP format standards.
* @enum
*/
Kekule.IO.Jcamp.Format = {
DX: 'dx',
CS: 'cs'
};
/**
* Enumeration of JCAMP data block types.
* @enum
*/
Kekule.IO.Jcamp.BlockType = {
/** Data block */
DATA: 0,
/** Link block */
LINK: 1
};
/**
* Enumeration of JCAMP cross reference target tyoes.
* @enum
*/
Kekule.IO.Jcamp.CrossRefType = {
SPECTRUM: 1,
STRUCTURE: 2,
UNKNOWN: 0
};
/**
* Enumeration of JCAMP ASDF data forms.
* @enum
*/
Kekule.IO.Jcamp.AsdfForm = {
AFFN: 1,
PAC: 2,
SQZ: 3,
DIF: 4,
SQZ_DUP: 13,
DIF_DUP: 14
};
/**
* Enumeration of JCAMP ASDF digit types.
* @enum
*/
Kekule.IO.Jcamp.DigitCharType = {
/*
SYMBOL_POSITIVE: '+',
SYMBOL_NEGTIVE: '-',
ASCII_DIGITS: {fromChar: '0', toChar: '9', fromDigit: 0},
SOZ_POSITIVE_DIGIT: {fromChar: '0', toChar: '9', fromDigit: 0},
*/
ASCII: 1,
PAC: 2,
SQZ: 3,
DIF: 4,
DUP: 5,
_DECIMAL_POINT: 9, // special mark, indicating a char is a decimal point
_ABNORMAL_VALUE: 19 // ? mark in data table, indicating a lost or abnormal value that need not to be parsed
};
var JcampDigitType = Kekule.IO.Jcamp.DigitCharType;
/**
* Storing the label map between JCAMP and Kekule spectrum.
* @class
*/
Kekule.IO.Jcamp.Labels = {
/** @private */
_maps: [], // each item is an array of [jcampLabel, kekuleLabel]
/**
* Returns all map items.
* @returns {Array}
*/
getMaps: function()
{
return JcampLabels._maps;
},
/**
* Add map items.
* @param {Array} items
*/
addMaps: function(items)
{
AU.pushUnique(JcampLabels._maps, items, true);
}
};
var JcampLabels = Kekule.IO.Jcamp.Labels;
/**
* Some utils methods about JCAMP.
* @class
*/
Kekule.IO.Jcamp.Utils = {
/**
* Check if two float values are equal in JCAMP file.
* @param {Number} v1
* @param {Number} v2
* @param {Number} allowedError
* @returns {Int}
*/
compareFloat: function(v1, v2, allowedError)
{
if (Kekule.ObjUtils.isUnset(allowedError))
allowedError = Math.max(Math.abs(v1), Math.abs(v2)) * 0.01; // TODO: current fixed to 1% of error
return Kekule.NumUtils.compareFloat(v1, v2, allowedError);
},
/**
* Calculate a suitable factor for converting to integers from a set of floats.
* This function is used for storing spectrum data into JCAMP-DX data table, or converting atom coord to integer in JCAMP-CS.
* @param {Number} minValue Min value of original data set.
* @param {Number} maxValue Max value of original data set.
* @param {Float} allowErrorRatio Permitted error, e.g. 0.001 for 0.1%.
* @param {Int} preferredScaleRangeMin If set, the rescaled data items should be larger than this value. It should be a negative value.
* @param {Int} preferredScaleRangeMax If set, the rescaled data items should be less than this value. It should be a positive value.
*/
calcNumFactorForRange: function(minValue, maxValue, allowedErrorRatio, preferredScaleRangeMin, preferredScaleRangeMax)
{
//var factor = Math.min(Math.abs(minValue), Math.abs(maxValue)) * allowedErrorRatio;
if (Kekule.NumUtils.isFloatEqual(minValue, maxValue))
{
if (Kekule.ObjUtils.notUnset(preferredScaleRangeMin) && Kekule.ObjUtils.notUnset(preferredScaleRangeMax))
{
if (minValue > preferredScaleRangeMin && maxValue < preferredScaleRangeMax)
return 1;
}
}
var factor = 1;
var errorBase = Math.abs(maxValue - minValue) || Math.max(Math.abs(maxValue), Math.abs(minValue));
if (allowedErrorRatio)
factor = errorBase * allowedErrorRatio;
//var factor = Math.min(allowedError / Math.abs(minValue), allowedError / Math.abs(maxValue));
if (Kekule.ObjUtils.notUnset(preferredScaleRangeMin) && Kekule.ObjUtils.notUnset(preferredScaleRangeMax))
{
if (minValue / factor > preferredScaleRangeMin && maxValue / factor < preferredScaleRangeMax) // we can even use a smaller factor?
{
var pfactor1 = Math.max(minValue / preferredScaleRangeMin, 0); // avoid negative factor
var pfactor2 = Math.max(maxValue / preferredScaleRangeMax, 0);
var pfactor = (!pfactor1) ? pfactor2 :
(!pfactor2) ? pfactor1 :
Math.max(pfactor1, pfactor2);
if (pfactor)
factor = Math.min(factor, pfactor);
}
}
//console.log(minValue, maxValue, factor);
return factor;
},
/**
* Remove all slashes, dashes, spaces, underlines and make all letters captializd.
* @param {String} labelName
* @returns {String}
*/
standardizeLdrLabelName: function(labelName)
{
var result = labelName.replace(/[\/\\\-\_\s]/g, '');
return result.toUpperCase();
},
/**
* Check if two LDR label names are same.
* @param {String} name1
* @param {String} name2
*/
ldrLabelNameEqual: function(name1, name2)
{
return JcampUtils.standardizeLdrLabelName(name1) === JcampUtils.standardizeLdrLabelName(name2);
},
/**
* Returns the core name and label type/data type/category of LDR.
* @param {String} labelName
* @param {Bool} checkDataType
* @returns {Hash} {coreName, labelType}
*/
analysisLdrLabelName: function(labelName, checkDataType)
{
var result;
if (labelName.startsWith(JcampConsts.SPECIFIC_LABEL_PREFIX))
result = {'coreName': JcampUtils.standardizeLdrLabelName(labelName.substr(JcampConsts.SPECIFIC_LABEL_PREFIX.length)), 'labelType': JLabelType.SPECIFIC, 'labelCategory': JLabelCategory.ANNOTATION};
else if (labelName.startsWith(JcampConsts.PRIVATE_LABEL_PREFIX))
result = {'coreName': JcampUtils.standardizeLdrLabelName(labelName.substr(JcampConsts.PRIVATE_LABEL_PREFIX.length)), 'labelType': JLabelType.PRIVATE, 'labelCategory': JLabelCategory.ANNOTATION};
else
result = {'coreName': labelName, 'labelType': JLabelType.GLOBAL, 'labelCategory': JLabelCategory.META};
if (checkDataType === undefined || checkDataType)
{
var detailInfo = JcampLabelTypeInfos.getInfo(result.coreName, result.labelType);
if (detailInfo)
{
result.dataType = detailInfo.dataType;
result.labelCategory = detailInfo.labelCategory;
}
}
//console.log('label info', result);
return result;
},
/**
* Get the corresponding info key name of Kekule spectrum for a JCAMP LDR name.
* @param {String} jcampName
* @param {String} spectrumType
* @returns {String}
*/
jcampLabelNameToKekule: function(jcampName, spectrumType)
{
var MetaPropNamespace = Kekule.Spectroscopy.MetaPropNamespace;
var jname = JcampUtils.standardizeLdrLabelName(jcampName);
if (jname.startsWith(JcampConsts.PRIVATE_LABEL_PREFIX)) // a private label
{
var coreName = jname.substr(JcampConsts.PRIVATE_LABEL_PREFIX.length);
return MetaPropNamespace.createPropertyName(MetaPropNamespace.CUSTOM, coreName);
}
else // need to check for map
{
var maps = JcampLabels.getMaps();
var candicateResult;
for (var i = 0, l = maps.length; i < l; ++i)
{
var map = maps[i];
if (jname === map[0])
{
var kname = map[1];
var kNameDetail = MetaPropNamespace.getPropertyNameDetail(kname);
if (spectrumType && kNameDetail.namespace && kNameDetail.namespace !== MetaPropNamespace.CUSTOM) // check if the spectrum type matches
{
if (spectrumType === kNameDetail.namespace)
return kname;
else
candicateResult = kname;
}
else
return kname;
}
}
if (candicateResult)
return candicateResult;
// not found
if (jname.startsWith(JcampConsts.SPECIFIC_LABEL_PREFIX)) // spectrum specific label
{
var coreName = jname.substr(JcampConsts.SPECIFIC_LABEL_PREFIX.length);
return MetaPropNamespace.createPropertyName(spectrumType, coreName);
}
else // global label
{
return MetaPropNamespace.createPropertyName('jcamp', jname);
}
}
},
/**
* Get the corresponding JCAMP LDR name for Kekule spectrum info property name.
* @param {String} kekuleName
* @param {String} spectrumType
* @param {Bool} convOnlyAssured
* @returns {String}
*/
kekuleLabelNameToJcamp: function(kekuleName, spectrumType, convOnlyAssured)
{
var MetaPropNamespace = Kekule.Spectroscopy.MetaPropNamespace;
var maps = JcampLabels.getMaps();
for (var i = 0, l = maps.length; i < l; ++i)
{
var map = maps[i];
if (kekuleName === map[1])
return map[0];
}
if (!convOnlyAssured)
{
// not found, regard it as private label
var nameDetail = MetaPropNamespace.getPropertyNameDetail(kekuleName);
return JcampConsts.PRIVATE_LABEL_PREFIX + nameDetail.coreName.toUpperCase();
}
else
return null;
},
/**
* Retrieve detail of cross reference from the text
* @param {String} crossRefText
* @returns {Hash} Including fields {refType, refTypeText, blockID}.
*/
getCrossReferenceDetail: function(crossRefText)
{
var parts = crossRefText.split(JcampConsts.CROSS_REF_TYPE_TERMINATOR);
var refTypeText = parts[0].trim();
var refType = (refTypeText.toUpperCase() === 'STRUCTURE')? Jcamp.CrossRefType.STRUCTURE: Jcamp.CrossRefType.SPECTRUM;
var blockPart = parts[1] && parts[1].trim();
var blockId;
if (blockPart && blockPart.toUpperCase().indexOf('BLOCK') >= 0) // here we check just word block, since the total key may be 'BLOCK_ID', 'BLOCK ID'...
{
var blockId = blockPart.split(JcampConsts.DATA_LABEL_TERMINATOR)[1];
if (blockId)
blockId = blockId.trim();
}
var result = {'refType': refType, 'refTypeText': refTypeText, 'blockId': blockId};
return result;
},
/**
* Add molecule to the {@link Kekule.Spectroscopy.Spectrum.refMolecules}.
* @param {Kekule.Spectroscopy.Spectrum} spectrum
* @param {Kekule.StructureFragment} molecule
*/
addMoleculeSpectrumCrossRef: function(spectrum, molecule)
{
var refMols = spectrum.getRefMolecules() || [];
if (refMols.indexOf(molecule) < 0)
{
refMols.push(molecule);
spectrum.setRefMolecules(refMols);
}
},
/**
* Returns the first non-empty string line of lines.
* @param {Array} lines
* @returns {string}
*/
getFirstNonemptyLine: function(lines)
{
var index = 0;
var result;
do
{
result = (lines[index] || '').trim();
++index;
}
while (!result && index < lines.length)
return result || '';
},
/**
* Returns the information and corresponding value of a ASDF char.
* @param {String} char String with length 1.
* @returns {Hash}
*/
getAsdfDigitInfo: function(char)
{
var result = {};
if (char === JcampConsts.VALUE_ABNORMAL_NUM)
{
result.digitType = JcampDigitType._ABNORMAL_VALUE;
result.value = null;
result.sign = 1;
}
else if (char === '.')
{
result.digitType = JcampDigitType._DECIMAL_POINT;
result.value = 0;
result.sign = 1;
}
else if (char >= '0' && char <= '9')
{
result.digitType = JcampDigitType.ASCII;
result.value = char.charCodeAt(0) - '0'.charCodeAt(0);
result.sign = 1;
}
else if (char === '+')
{
result.digitType = JcampDigitType.PAC;
result.value = 0;
result.sign = 1;
}
else if (char === '-')
{
result.digitType = JcampDigitType.PAC;
result.value = 0;
result.sign = -1;
}
else if (char === '@')
{
result.digitType = JcampDigitType.SQZ;
result.value = 0;
result.sign = 1;
}
else if (char >= 'A' && char <= 'I')
{
result.digitType = JcampDigitType.SQZ;
result.value = char.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
result.sign = 1;
}
else if (char >= 'a' && char <= 'i')
{
result.digitType = JcampDigitType.SQZ;
result.value = char.charCodeAt(0) - 'a'.charCodeAt(0) + 1;
result.sign = -1;
}
else if (char === '%')
{
result.digitType = JcampDigitType.DIF;
result.value = 0;
result.sign = 1;
}
else if (char >= 'J' && char <= 'R')
{
result.digitType = JcampDigitType.DIF;
result.value = char.charCodeAt(0) - 'J'.charCodeAt(0) + 1;
result.sign = 1;
}
else if (char >= 'j' && char <= 'r')
{
result.digitType = JcampDigitType.DIF;
result.value = char.charCodeAt(0) - 'j'.charCodeAt(0) + 1;
result.sign = -1;
}
else if (char >= 'S' && char <= 'Z')
{
result.digitType = JcampDigitType.DUP;
result.value = char.charCodeAt(0) - 'S'.charCodeAt(0) + 1;
result.sign = 1;
}
else if (char === 's')
{
result.digitType = JcampDigitType.DUP;
result.value = 9;
result.sign = 1;
}
else // other chars
{
result.digitType = null;
result.value = null;
}
return result;
},
/** @private */
_calcAsdfNumber: function(asdfInfoGroup)
{
var ratio = 1;
var value = 0;
var decimalPointRatio = 1;
var decimalPointCount = 0;
var digitType;
for (var i = asdfInfoGroup.length - 1; i >= 0; --i)
{
var info = asdfInfoGroup[i];
if (info.digitType === JcampDigitType._DECIMAL_POINT) // special handling of decimal point(.) in number chars
{
decimalPointRatio = ratio;
++decimalPointCount;
if (decimalPointCount > 1) // more than one decimal point, error
return null;
}
else
{
value += info.value * ratio;
ratio *= 10;
digitType = info.digitType; // store the digitType of the head first item
}
}
var sign = asdfInfoGroup[0].sign;
value *= sign / decimalPointRatio;
var result = {'value': value, 'digitType': digitType};
// console.log(asdfInfoGroup, result, asdfInfoGroup[0].sign);
return result;
},
/** @private */
_pushAsdfNumberToSeq: function(seq, numInfo, prevNumInfo)
{
var result = {'seq': seq, 'lastValueType': numInfo.digitType}; // returns the last digit type, it may be used in Y-checked of XYData
if (numInfo.digitType === JcampDigitType._ABNORMAL_VALUE)
{
seq.push(JcampConsts.UNKNOWN_VALUE);
}
else if (numInfo.digitType === JcampDigitType.ASCII || numInfo.digitType === JcampDigitType.PAC
|| numInfo.digitType === JcampDigitType.SQZ)
{
seq.push(numInfo.value);
}
else
{
if (!seq.length) // DIF or DUP require prev number, here fail to handle
return false;
var lastValue = seq[seq.length - 1];
if (numInfo.digitType === JcampDigitType.DIF)
{
seq.push(lastValue + numInfo.value);
}
else if (numInfo.digitType === JcampDigitType.DUP)
{
if (prevNumInfo.digitType === JcampDigitType.PAC || prevNumInfo.digitType === JcampDigitType.DUP || !prevNumInfo.digitType)
return false;
for (var i = 1; i < numInfo.value; ++i)
{
// seq.push(lastValue);
result = JcampUtils._pushAsdfNumberToSeq(seq, prevNumInfo, null);
}
// when DUP, the lastValueType should returns the one of prevNumInfo
if (prevNumInfo)
result.lastValueType = prevNumInfo.digitType;
}
else // unknown type
return false;
}
return result;
},
/**
* Parse a AFFN or ASDF line, returns number array.
* @param {String} str
* @returns {Array}
*/
decodeAsdfLine: function(str)
{
var result = [];
var currAsdfGroup = [];
var prevNumInfo = null;
var _pushToSeq = function(numSeq, asdfGroup, prevNumInfo)
{
if (asdfGroup.length)
{
var result;
var numInfo = JcampUtils._calcAsdfNumber(asdfGroup);
if (numInfo)
result = JcampUtils._pushAsdfNumberToSeq(numSeq, numInfo, prevNumInfo);
if (!numInfo || !result) // has error
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_ASDF_FORMAT_ERROR_WITH_STR').format(str));
// save the last digitType in numSeq, since it may be used in Y-check of XYData
numSeq.__$lastValueType__ = result.lastValueType;
return numInfo;
/*
if (!numInfo || !JcampUtils._pushAsdfNumberToSeq(numSeq, numInfo, prevNumInfo)) // has error
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_ASDF_FORMAT_ERROR_WITH_STR').format(str));
return numInfo;
*/
}
};
for (var i = 0, l = str.length; i < l; ++i)
{
var digitInfo = JcampUtils.getAsdfDigitInfo(str.charAt(i));
if (!digitInfo.digitType) // unknown type, may be a space or delimiter?
{
// decode curr group and start a new blank one
prevNumInfo = _pushToSeq(result, currAsdfGroup, prevNumInfo);
currAsdfGroup = [];
}
else if (digitInfo.digitType === JcampDigitType._ABNORMAL_VALUE) // ? mark, a unknown number
{
prevNumInfo = _pushToSeq(result, currAsdfGroup, prevNumInfo);
_pushToSeq(result, [digitInfo], null); // push a unknown digit
currAsdfGroup = [];
}
else if (digitInfo.digitType === JcampDigitType._DECIMAL_POINT)
{
currAsdfGroup.push(digitInfo);
}
else if (digitInfo.digitType === JcampDigitType.ASCII) // 0-9, push to curr group
{
currAsdfGroup.push(digitInfo);
}
else // PAC/DIF/SQZ/DUP, need to end curr group and start a new one
{
prevNumInfo = _pushToSeq(result, currAsdfGroup, prevNumInfo);
currAsdfGroup = [digitInfo];
}
}
// last tailing group
prevNumInfo = _pushToSeq(result, currAsdfGroup, prevNumInfo);
// console.log('seq: ', result);
return result;
},
/**
* Encode an array of integes into ASDF format.
* @param {Array} numbers
* @param {String} format Compression format.
* @returns {String}
*/
encodeAsdfLine: function(numbers, format)
{
var F = Kekule.IO.Jcamp.AsdfForm;
if (format === F.AFFN)
return JcampUtils._encodeNumbersToAffnLine(numbers);
else if (format === F.PAC)
return JcampUtils._encodeNumbersToPacLine(numbers);
else if (format === F.SQZ)
return JcampUtils._encodeNumbersToSqzLine(numbers, false);
else if (format === F.SQZ_DUP)
return JcampUtils._encodeNumbersToSqzLine(numbers, true);
else if (format === F.DIF)
return JcampUtils._encodeNumbersToDifLine(numbers, false);
else if (format === F.DIF_DUP)
return JcampUtils._encodeNumbersToDifLine(numbers, true);
else // default
return JcampUtils._encodeNumbersToDifLine(numbers, true);
},
/** @private */
_encodeNumbersToAffnLine: function(numbers)
{
var seq = [];
for (var i = 0, l = numbers.length; i < l; ++i)
{
var n = numbers[i];
var s = (Kekule.NumUtils.isNormalNumber(n))? n.toString(): JcampConsts.VALUE_ABNORMAL_NUM;
seq.push(s);
}
return seq.join(' ');
},
/** @private */
_encodeNumbersToPacLine: function(numbers)
{
var strs = [];
for (var i = 0, l = numbers.length; i < l; ++i)
{
var s;
var n = numbers[i];
if (Kekule.NumUtils.isNormalNumber(n))
{
var sign = (n >= 0)? '+': '-';
s = sign + Math.abs(n).toString();
}
else
s = JcampConsts.VALUE_ABNORMAL_NUM;
strs.push(s);
}
return strs.join('');
},
/** @private */
_encodeNumbersToSqzLine: function(numbers, enableDup)
{
return JcampUtils._numSeqToCompressedFormString(numbers, 'A', 'a', '@', enableDup);
},
/** @private */
_encodeNumbersToDifLine: function(numbers, enableDup)
{
/*
var sHead = JcampUtils._encodeNumbersToSqzLine([numbers[0]]); // the first number should always be in SQZ form
var difSeq = [];
var last = 0;
for (var i = 0, l = numbers.length; i < l; ++i)
{
var n = numbers[i];
if (Kekule.NumUtils.isNormalNumber(n))
{
var dif = numbers[i] - last;
difSeq.push(dif);
last = n;
}
else
{
difSeq.push(n);
last = 0;
}
}
var result = sHead + JcampUtils._numSeqToCompressedFormString(difSeq, 'J', 'j', '%', enableDup);
return result;
*/
// we need to split the numbers to several sub sequences, at the heading and when meeting the abnormal number,
// a new seq should be created and starting with a SQZ number
var subsets = [];
var currSet = {'seq': []};
subsets.push(currSet);
var pushToCurrSet = function(num)
{
var isNormalNum = Kekule.NumUtils.isNormalNumber(num);
if (!isNormalNum)
{
if (Kekule.ObjUtils.isUnset(currSet.head))
currSet.head = num;
else
currSet.seq.push(num);
currSet = {'seq': []}; // create new set
subsets.push(currSet);
}
else // is normal number
{
if (Kekule.ObjUtils.isUnset(currSet.head))
{
currSet.head = num;
}
else
{
currSet.seq.push(num - currSet.last);
}
currSet.last = num;
}
}
for (var i = 0, l = numbers.length; i < l; ++i)
{
var n = numbers[i];
pushToCurrSet(n);
}
// iterate subsets
var strs = [];
for (var i = 0, l = subsets.length; i < l; ++i)
{
var subset = subsets[i];
if (Kekule.ObjUtils.notUnset(subset.head)) // subset not empty
{
strs.push(JcampUtils._encodeNumbersToSqzLine([subset.head]));
if (subset.seq.length)
strs.push(JcampUtils._numSeqToCompressedFormString(subset.seq, 'J', 'j', '%', enableDup));
}
}
return strs.join('');
},
/** @private */
_numToCompressedFormString: function(number, positiveBase, negativeBase, zeroBase)
{
// assume number is a int
if (!Kekule.NumUtils.isNormalNumber(number))
return JcampConsts.VALUE_ABNORMAL_NUM;
else if (number === 0)
return zeroBase;
var s = Math.abs(number).toString();
var firstDigit = Kekule.NumUtils.getHeadingDigit(number);
var sFirst = (number > 0)? String.fromCharCode(positiveBase.charCodeAt(0) + (firstDigit - 1)):
String.fromCharCode(negativeBase.charCodeAt(0) + (firstDigit - 1));
s = sFirst + s.substr(1);
return s;
},
/** @private */
_numSeqToCompressedFormString: function(numSeq, positiveBase, negativeBase, zeroBase, enableDup)
{
var seq = [];
var lastNum;
var dupCount = 0;
var dupChars = ['S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 's']; // The dup 1 (S) is actually useless
for (var i = 0, l = numSeq.length; i < l; ++i)
{
var dupHandled = false;
var n = numSeq[i];
if (enableDup)
{
if (n === lastNum) // NaN !== NaN
{
++dupCount;
dupHandled = true;
}
else
{
if (dupCount) // output last dup
{
seq.push(dupChars[dupCount]);
dupCount = 0;
}
}
}
if (!dupHandled) // output the normal number
{
var s = JcampUtils._numToCompressedFormString(n, positiveBase, negativeBase, zeroBase);
seq.push(s);
}
lastNum = n;
}
if (enableDup && dupCount)
seq.push(dupChars[dupCount]);
return seq.join('');
},
/**
* Parse a AFFN or ASDF table, returns array of number groups ([[a,b,c], [e,f,g]]).
* @param {Array} strLines
* @param {Hash} options Additional options for value check.
* @returns {Array}
*/
decodeAsdfTableLines: function(strLines, options)
{
var op = options || {};
var result = [];
var buffer = [];
var isNextContiLine = false;
var prevEndWithDif = false;
var abscissaInterval;
var lineCount = strLines.length;
var appendDecodedBufferToResult = function(result, buffer, doAbscissaValueCheck, doOrdinateValueCheck, prevEndWithDif, totalLineCount)
{
//if (doOrdinateValueCheck)
if (prevEndWithDif) // prev line end with DIF form, next line should has a duplicated value check item
{
var lastValues = result[result.length - 1];
//console.log('lastValues', lastValues, result);
if (lastValues && lastValues.length)
{
if (doOrdinateValueCheck)
{
// check the last value of prev line and first ordinate value of this line
var prevEndOrdinateValue = lastValues[lastValues.length - 1];
var currHeadOrdinateValue = buffer[1];
//console.log(doOrdinateValueCheck, prevEndOrdinateValue, currHeadOrdinateValue);
if ((typeof (prevEndOrdinateValue) === 'number' && Kekule.ObjUtils.notUnset(prevEndOrdinateValue))
&& (typeof (currHeadOrdinateValue) === 'number' && Kekule.ObjUtils.notUnset(currHeadOrdinateValue)))
{
if (!Kekule.NumUtils.isFloatEqual(prevEndOrdinateValue, currHeadOrdinateValue))
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_Y_VALUE_CHECK_ERROR'));
else // check passed, remove the tailing check value of previous line, not the heading Y value of this line!
{
lastValues.pop();
}
}
}
else // bypass the check, but still remove the duplicated ordinate value
{
lastValues.pop();
}
}
}
if (doAbscissaValueCheck)
checkAbscissaInterval(buffer, result[result.length - 1], totalLineCount);
/*
if (result[result.length - 1])
console.log('push buffer', result[result.length - 1], (buffer[0] - result[result.length - 1][0]) / (result[result.length - 1].length - 1));
*/
result.push(buffer);
}
var checkAbscissaInterval = function(currGroup, prevGroup, lineCount)
{
if (currGroup && prevGroup)
{
var curr = currGroup[0];
var prev = prevGroup[0];
var currInterval = (curr - prev) / (prevGroup.length - 1); // the first item in prevGroup is X, so here we use length-1
// console.log('prev interval', abscissaInterval, 'curr', currInterval);
if (abscissaInterval)
{
var abscissaRange = Math.min(Math.abs(currInterval), Math.abs(abscissaInterval)) * (lineCount || 1);
var allowedError = abscissaRange * (Kekule.globalOptions.IO.jcamp.dataValueCheckAllowedErrorRatio || 0.0001);
//if (!Kekule.NumUtils.isFloatEqual(currInterval, abscissaInterval, allowedError))
if (JcampUtils.compareFloat(currInterval, abscissaInterval, allowedError) !== 0)
{
console.log('X check error', currInterval, abscissaInterval, allowedError);
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_X_VALUE_CHECK_ERROR'));
}
}
else
abscissaInterval = currInterval;
}
return true;
};
for (var i = 0, l = strLines.length; i < l; ++i)
{
var currLine = strLines[i].trim();
if (!currLine)
continue;
var endWithContiMark = (currLine.endsWith(JcampConsts.TABLE_LINE_CONTI_MARK)); // end with a conti-mark?
if (endWithContiMark)
currLine = currLine.substr(0, currLine.length - JcampConsts.TABLE_LINE_CONTI_MARK.length);
var decodeValues = JcampUtils.decodeAsdfLine(currLine);
if (isNextContiLine) // continue from last, put decode values to buffer
buffer = buffer.concat(decodeValues);
else
buffer = decodeValues;
isNextContiLine = endWithContiMark;
if (!isNextContiLine) // no continous line, do value check and put buffer to result
{
appendDecodedBufferToResult(result, buffer, op.doValueCheck, op.doValueCheck && prevEndWithDif, prevEndWithDif, lineCount);
buffer = [];
}
prevEndWithDif = decodeValues.__$lastValueType__ === JcampDigitType.DIF; // if prev line ends with DIF, may need to do a value check on next line
}
if (buffer.length) // last unhandled buffer
appendDecodedBufferToResult(result, buffer, op.doValueCheck, op.doValueCheck && prevEndWithDif, prevEndWithDif, lineCount);
return result;
},
/**
* Encode a AFFN or ASDF table, returns array of strings ([line1, line2]).
* @param {Array} numLines Each item is a line of numbers.
* @param {Int} asdfForm
* @param {Hash} options Additional options for encode. Can include fields: {abscissaFirst}
* @returns {Array}
*/
encodeAsdfTableLines: function(numLines, asdfForm, options)
{
var op = options || {};
var result = [];
for (var i = 0, l = numLines.length; i < l; ++i)
{
var numbers = numLines[i];
var numSeq;
var s = '';
if (numbers.length)
{
if (op.abscissaFirst) // the first number is abscissa value while the rest are ordinates
{
s += JcampUtils._encodeNumbersToAffnLine([numbers[0]]) + ' '; // the first is always be AFFN
numSeq = numbers.slice(1);
}
else
numSeq = numbers;
s += JcampUtils.encodeAsdfLine(numSeq, asdfForm);
}
result.push(s);
}
//console.log('encode', numLines, result);
return result;
},
/**
* Convert a line of data string in XYPOINTS/PEAK TABLE LDR.
* Usually the values are divided into groups with delimiter semicolons or space,
* and each item in a group is an AFFN value or string delimited by comma.
* @param {String} str
* @returns {Array}
*/
decodeAffnGroupLine: function(str)
{
var CharTypes = {
DIGIT: 1,
STRING: 2,
ENCLOSED_STRING: 3,
STR_ENCLOSER_LEADING: 11,
STR_ENCLOSER_TAILING: 12,
ITEM_DELIMITER: 21,
GROUP_DELIMITER: 22,
BLANK: 30,
OTHER: 40
};
var input = str.trim();
// if input surrounded with '()' (e.g., in peak assignment), removes them first
if (input.startsWith(JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_LEADING) && input.endsWith(JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_TAILING))
input = input.substr(JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_LEADING.length, input.length - JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_LEADING.length - JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_TAILING.length);
var currToken = {
'tokenType': null,
'text': ''
};
var result = [];
var currGroup = [];
var parseCurrToken = function(tokenStr, tokenType)
{
return tokenStr?
((tokenType === CharTypes.DIGIT)? parseFloat(tokenStr): tokenStr):
undefined;
};
var parseAndPushCurrTokenToGroup = function(allowEmpty)
{
var v;
if (allowEmpty || currToken.text)
{
v = parseCurrToken(currToken.text, currToken.tokenType);
currGroup.push(v);
}
// clear curr token info
currToken.tokenType = null;
currToken.text = '';
return v;
};
var pushCurrGroup = function()
{
if (currGroup.length)
result.push(currGroup);
currGroup = [];
};
var getCharType = function(c, insideEnclosedString, insideExplicitGroup)
{
if (insideEnclosedString)
return c.match(JcampConsts.GROUPED_VALUE_STR_ENCLOSER_TAILING)? CharTypes.STR_ENCLOSER_TAILING: CharTypes.STRING;
else
return (c >= '0' && c <= '9' || c === '.')? CharTypes.DIGIT:
c.match(/\s/)? CharTypes.BLANK:
c.match(JcampConsts.GROUPED_VALUE_STR_ENCLOSER_LEADING)? CharTypes.STR_ENCLOSER_LEADING:
c.match(JcampConsts.GROUPED_VALUE_STR_ENCLOSER_TAILING)? CharTypes.STR_ENCLOSER_TAILING:
c.match(JcampConsts.GROUPED_VALUE_GROUP_DELIMITER_PATTERN)? (insideExplicitGroup? CharTypes.ITEM_DELIMITER: CharTypes.GROUP_DELIMITER):
c.match(JcampConsts.GROUPED_VALUE_ITEM_DELIMITER_PATTERN)? CharTypes.ITEM_DELIMITER:
CharTypes.STRING;
};
var prevIsBlankChar = false;
for (var i = 0, l = input.length; i < l; ++i)
{
var c = input.charAt(i);
var charType = getCharType(c, currToken.tokenType === CharTypes.ENCLOSED_STRING);
if (charType === CharTypes.BLANK)
{
if (currToken.tokenType === CharTypes.ENCLOSED_STRING)
{
currToken.text += c;
charType = currToken.tokenType;
}
else // pending decision until next token char
{
}
}
if (charType < CharTypes.STR_ENCLOSER_LEADING) // normal chars
{
if (prevIsBlankChar && currToken.text) // blank between normal tokens, should be group delimiter?
{
parseAndPushCurrTokenToGroup();
pushCurrGroup();
}
if (!currToken.tokenType)
currToken.tokenType = charType;
else
currToken.tokenType = Math.max(currToken.tokenType, charType); // if both string and digit type ocurrs, the token type shoud be string
currToken.text += c;
}
else if (charType < CharTypes.ITEM_DELIMITER) // < or >
{
if (charType === CharTypes.STR_ENCLOSER_LEADING)
{
if (prevIsBlankChar && currToken.text) // blank before '<', should be group delimiter?
{
parseAndPushCurrTokenToGroup();
pushCurrGroup();
}
else
parseAndPushCurrTokenToGroup();
currToken.tokenType = CharTypes.ENCLOSED_STRING;
}
else // if (charType === CharTypes.STR_ENCLOSER_TAILING)
{
//parseAndPushCurrTokenToGroup(true);
currToken.tokenType = CharTypes.STRING;
}
}
else // delimiter
{
if (charType === CharTypes.ITEM_DELIMITER)
{
parseAndPushCurrTokenToGroup(true);
}
else if (charType === CharTypes.GROUP_DELIMITER)
{
parseAndPushCurrTokenToGroup(true);
pushCurrGroup();
}
}
prevIsBlankChar = (charType === CharTypes.BLANK);
}
// at last the remaining token
parseAndPushCurrTokenToGroup();
pushCurrGroup();
return result;
},
/**
* Encode a line of data string for XYPOINTS/PEAK TABLE LDR.
* Usually the values are numbers, but it may contain strings too.
* @param {Array} values
* @param {Hash} options;
* @returns {String}
*/
encodeAffnGroupLine: function(values, options)
{
var parts = [];
for (var i = 0, l = values.length; i < l; ++i)
{
var v = values[i];
var sv;
if (typeof(v) === 'number')
sv = Kekule.NumUtils.isNormalNumber(v)? v.toString(): JcampConsts.VALUE_ABNORMAL_NUM;
else // a string value
sv = JcampConsts.GROUPED_VALUE_STR_ENCLOSER_LEADING + v.toString() + JcampConsts.GROUPED_VALUE_STR_ENCLOSER_TAILING;
parts.push(sv);
}
return parts.join(JcampConsts.GROUPED_VALUE_ITEM_DELIMITER);
},
/**
* Parse lines of data string in XYPOINTS/PEAK TABLE LDR.
* Usually the values are divided into groups with delimiter semicolons or space,
* and each item in a group is an AFFN value or string delimited by comma.
* @param {Array} lines
* @param {Hash} options Unused now.
* @returns {Array}
*/
decodeAffnGroupTableLines: function(lines, options)
{
var result = [];
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i];
result = result.concat(JcampUtils.decodeAffnGroupLine(line));
}
return result;
},
/**
* Encode strings from value groups for XYPOINTS/PEAK TABLE LDR.
* @param {Array} valueGroups Each item is a child array of individual values.
* @param {Hash} options;
* @returns {Array}
*/
encodeAffnGroupTableLines: function(valueGroups, options)
{
var result = [];
for (var i = 0, l = valueGroups.length; i < l; ++i)
{
var group = valueGroups[i];
var s = JcampUtils.encodeAffnGroupLine(group, options);
if (options && options.explicitlyEnclosed)
s = JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_LEADING + s + JcampConsts.GROUPED_VALUE_EXPLICIT_GROUP_TAILING;
result.push(s);
}
return result;
},
/**
* Returns details about variable name and format from the format text.
* @param {String} formatText Such as (X++(Y..Y)), (XY..XY), etc.
* @returns {Hash}
*/
getDataTableFormatDetails: function(formatText)
{
/*
##XYDATA: (X++(Y..Y))
##XYPOINTS: (XY..XY)
##PEAK TABLE: (XY..XY)
##PEAK ASSIGNMENTS: (XYA) or (XYWA)
*/
if (!formatText.startsWith(JcampConsts.DATA_FORMAT_GROUP_LEADING) && !formatText.endsWith(JcampConsts.DATA_FORMAT_GROUP_TAILING))
return Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_VAR_LIST_FORMAT_ERROR', formatText));
else
{
var text = formatText.substr(JcampConsts.DATA_FORMAT_GROUP_LEADING.length, formatText.length - JcampConsts.DATA_FORMAT_GROUP_LEADING.length - JcampConsts.DATA_FORMAT_GROUP_TAILING.length);
// remove all internal spaces in text
text = text.replace(/\s/g, '');
// test XYData format
var patternXYData = /^([a-zA-Z])\+\+\(([a-zA-Z])\.\.([a-zA-Z])\)$/;
var matchResult = text.match(patternXYData);
if (matchResult)
{
if (matchResult[2] !== matchResult[3]) // Y..Y
return Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_VAR_LIST_FORMAT_ERROR', formatText));
else
return {'format': JcampConsts.DATA_VARLIST_FORMAT_XYDATA, 'varInc': matchResult[1], 'varLoop': matchResult[2], 'vars': [matchResult[1], matchResult[2]]};
}
else
{
var patternXYPoint = /^([a-zA-Z]+)\s*\.\.\s*([a-zA-Z]+)$/;
var matchResult = text.match(patternXYPoint);
if (matchResult)
{
if (matchResult[1] !== matchResult[2]) // XY..XY
return Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_VAR_LIST_FORMAT_ERROR', formatText));
else
{
var vars = [];
for (var i = 0, l = matchResult[1].length; i < l; ++i)
{
if (!matchResult[1].charAt(i).match(/\s/))
vars.push(matchResult[1].charAt(i));
}
return (vars.length <= 2)?
{'format': JcampConsts.DATA_VARLIST_FORMAT_XYPOINTS, 'vars': vars}:
{'format': JcampConsts.DATA_VARLIST_FORMAT_XYWPOINTS, 'vars': vars};
}
}
else
{
var patternGroupList = /^(([a-zA-Z]\,?)+)$/;
var matchResult = text.match(patternGroupList);
if (matchResult)
{
var vars = [];
for (var i = 0, l = matchResult[1].length; i < l; ++i)
{
if (matchResult[1].charAt(i) !== ',')
vars.push(matchResult[1].charAt(i));
}
return {'format': JcampConsts.DATA_VARLIST_FORMAT_VAR_GROUPS, 'vars': vars};
}
}
}
return Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_VAR_LIST_FORMAT_UNSUPPORTED', formatText));
}
},
/**
* Generate a data table format string from format and var symbols.
* @param {Int} format
* @param {Array} varSymbols
* @returns {String} Format string such as (X++(Y..Y)), (XY..XY), etc.
*/
generateDataTableFormatDescriptor: function(format, varSymbols)
{
if (format === JcampConsts.DATA_VARLIST_FORMAT_XYDATA)
{
return (JcampConsts.DATA_FORMAT_GROUP_LEADING + '{0}++' + JcampConsts.DATA_FORMAT_GROUP_LEADING +
'{1}' + JcampConsts.DATA_FORMAT_LOOP + '{1}' + JcampConsts.DATA_FORMAT_GROUP_TAILING + JcampConsts.DATA_FORMAT_GROUP_TAILING).format(varSymbols[0], varSymbols[1]);
}
else if (format === JcampConsts.DATA_VARLIST_FORMAT_XYPOINTS || format === JcampConsts.DATA_VARLIST_FORMAT_XYWPOINTS)
{
var varListTemplateParts = [];
for (var i = 0, l = varSymbols.length; i < l; ++i)
{
varListTemplateParts.push('{' + i + '}');
}
var varListTemplate = varListTemplateParts.join('');
var template = JcampConsts.DATA_FORMAT_GROUP_LEADING + varListTemplate + JcampConsts.DATA_FORMAT_LOOP + varListTemplate + JcampConsts.DATA_FORMAT_GROUP_TAILING;
return template.format.apply(template, varSymbols);
}
else if (format === JcampConsts.DATA_VARLIST_FORMAT_VAR_GROUPS)
{
return JcampConsts.DATA_FORMAT_GROUP_LEADING + varSymbols.join('') + JcampConsts.DATA_FORMAT_GROUP_TAILING;
}
else
return '';
},
/**
* Returns details about variable name/format and plot descriptor from the format text of DATA TABLE LDR.
* @param {String} formatText Such as (X++(Y..Y)), XYDATA; (XY..XY), XYPOINTS etc.
* @returns {Hash}
*/
getDataTableFormatAndPlotDetails: function(formatText)
{
//var s = formatText.replace(/\s/g, ''); // remove all blanks first
// The format part is always enclosed by '()', extract it first
var p1 = formatText.indexOf(JcampConsts.DATA_FORMAT_GROUP_LEADING);
var p2 = formatText.lastIndexOf(JcampConsts.DATA_FORMAT_GROUP_TAILING);
if (p1 >= 0 && p2 >= 0)
{
var sFormat = formatText.substring(p1, p2 + 1);
var result = JcampUtils.getDataTableFormatDetails(sFormat);
// then the plot descriptor
var sPlotDescriptor = formatText.substr(p2 + 1).trim();
var p3 = sPlotDescriptor.indexOf(JcampConsts.DATA_FORMAT_PLOT_DESCRIPTOR_DELIMITER);
if (p3 >= 0)
{
sPlotDescriptor = sPlotDescriptor.substr(JcampConsts.DATA_FORMAT_PLOT_DESCRIPTOR_DELIMITER.length);
result.plotDescriptor = sPlotDescriptor;
}
else // no plot descriptor, do nothing here
{
}
return result;
}
else
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_DATA_TABLE_VAR_LIST_FORMAT_ERROR', formatText));
},
/**
* Generate a data table format string and plot string from format and var symbols.
* @param {Int} format
* @param {Array} varSymbols
* @returns {Hash} Such as {format: '(X++(Y..Y))', plot: 'XYDATA'}.
*/
generateDataTableFormatAndPlotDescriptors: function(format, varSymbols)
{
var sFormat = JcampUtils.generateDataTableFormatDescriptor(format, varSymbols);
var sPlot;
if (format === JcampConsts.DATA_VARLIST_FORMAT_XYDATA)
sPlot = 'XYDATA';
else if (format === JcampConsts.DATA_VARLIST_FORMAT_XYPOINTS)
sPlot = 'XYPOINTS';
else if (format === JcampConsts.DATA_VARLIST_FORMAT_VAR_GROUPS)
{
if (varSymbols.length <= 2)
sPlot = 'PEAK TABLE';
else
sPlot = 'PEAK ASSIGNMENTS'
}
return {'format': sFormat, 'plot': sPlot};
},
/**
* Generate a data table format string and plot from format and var symbols.
* @param {Int} format
* @param {Array} varSymbols
* @returns {String} Format string such as (X++(Y..Y)), XYDATA ; (XY..XY), XYPOINTS etc.
*/
generateDataTableFormatAndPlotString: function(format, varSymbols)
{
var details = JcampUtils.generateDataTableFormatAndPlotDescriptors(format, varSymbols);
return details.format + JcampConsts.DATA_FORMAT_PLOT_DESCRIPTOR_DELIMITER + details.plot;
}
};
var JcampUtils = Kekule.IO.Jcamp.Utils;
Kekule.IO.Jcamp.BlockUtils = {
// methods about JCAMP block object from analysis tree
/**
* Create a new JCAMP block object.
* @param {Object} parent Parent block.
* @returns {Object}
*/
createBlock: function(parent)
{
return {'blocks': [], 'ldrs': [], 'ldrIndexes': {}, '_parent': parent}
},
/** @private */
setBlockParent: function(block, parent)
{
block._parent = parent;
return block;
},
/**
* Returns the index of label in block.
* @param {String} labelName
* @param {Object} block
* @returns {Int}
*/
getLabelIndex: function(labelName, block)
{
return block.ldrIndexes[labelName] || -1;
},
/**
* Get the LDR object at index of block.
* @param {Int} index
* @param {Object} block
* @returns {Object}
*/
getLdrAt: function(index, block)
{
return block.ldrs[index];
},
/**
* Add a ldr record to JCAMP block.
* @param {Object} block
* @param {Object} ldrObj Should has fields {labelName, valueLines}.
*/
addLdrToBlock: function(block, ldrObj)
{
block.ldrs.push(ldrObj);
block.ldrIndexes[ldrObj.labelName] = block.ldrs.length - 1;
},
/**
* Returns the nested level of a JCAMP analysis tree.
* @param {Object} analysisTree
* @returns {Int}
*/
getNestedBlockLevelCount: function(analysisTree)
{
var result = 1;
var blocks = analysisTree.blocks;
if (blocks && blocks.length)
{
var maxSubBlockLevelCount = 0;
for (var i = 0, l = blocks.length; i < l; ++i)
{
var c = JcampBlockUtils.getNestedBlockLevelCount(blocks[i]);
if (c > maxSubBlockLevelCount)
maxSubBlockLevelCount = c;
}
result += maxSubBlockLevelCount;
}
return result;
},
/**
* Returns the specified LDR of a block.
* @param {Object} block
* @param {String} labelName
* @returns {Hash}
*/
getBlockLdr: function(block, labelName)
{
var index = block.ldrIndexes[labelName];
return (index >= 0)? block.ldrs[index]: null;
},
/**
* Returns the meta info (type, format...) of a block.
* @param {Object} block
* @returns {Hash}
*/
getBlockMeta: function(block)
{
var result = block.meta;
if (!result)
{
result = {
'blockType': block.blocks.length ? Jcamp.BlockType.LINK : Jcamp.BlockType.DATA,
'format': block.ldrIndexes[JcampConsts.LABEL_DX_VERSION]? Jcamp.Format.DX :
block.ldrIndexes[JcampConsts.LABEL_CS_VERSION]? Jcamp.Format.CS :
null // unknown format
};
block.meta = result;
}
return result;
}
};
var JcampBlockUtils = Kekule.IO.Jcamp.BlockUtils;
/**
* Enumeration of LDR value types
* @enum
*/
Kekule.IO.Jcamp.ValueType = {
AFFN: 1, // single line AFFN value
ASDF: 2,
AFFN_ASDF: 3,
MULTILINE_AFFN_ASDF: 5, // AFFN or ASDF in multiple lines, e.g. in XYDATA, with a leading format line
MULTILINE_AFFN_GROUP: 6, // AFFN/string group, e.g. in XYPOINTS or PEAKTABLE, with a leading format line
SIMPLE_AFFN_GROUP: 7, // AFFN group, without leading format line
STRING_GROUP: 8, // string group, delimited by comma, e.g., many NTUPLES var definition LDRs
DATA_TABLE: 9, // NTuple data table, the format varies according to format string of the first line
STRING: 10,
SHORT_DATE: 21, // date string in format YY/MM/DD
SHORT_TIME: 22, // time string in format hh:mm:ss
DATETIME: 23, // data/time string in format YYYY/MM/DD [HH:MM:SS[.SSSS] [±UUUU]]
NONE: 0 // special marks, value should be ignored
};
var JValueType = Kekule.IO.Jcamp.ValueType;
/**
* Enumeration of JCAMP label types.
* @enum
*/
Kekule.IO.Jcamp.LabelType = {
GLOBAL: 0,
SPECIFIC: 1,
PRIVATE: 2
}
var JLabelType = Kekule.IO.Jcamp.LabelType;
/**
* Enumeration of JCAMP label categories.
* @enum
*/
Kekule.IO.Jcamp.LabelCategory = {
GLOBAL: 'global',
META: 'meta',
PARAMTER: 'parameter',
CONDITION: 'condition',
ANNOTATION: 'annotation'
};
var JLabelCategory = Kekule.IO.Jcamp.LabelCategory;
Kekule.IO.Jcamp.LabelTypeInfos = {
_DEFAULT_TYPE: JValueType.STRING
};
Kekule.IO.Jcamp.LabelTypeInfos.createInfo = function(labelName, dataType, labelType, labelCategory, specificType)
{
var name = JcampUtils.standardizeLdrLabelName(labelName);
var defaultLabelType = specificType? JLabelType.SPECIFIC: JLabelType.GLOBAL;
if (!labelType)
labelType = defaultLabelType;
if (!labelCategory)
labelCategory = (labelType === JLabelType.GLOBAL)? JLabelCategory.META: JLabelCategory.ANNOTATION;
if (labelType === JLabelType.SPECIFIC && !name.startsWith(JcampConsts.SPECIFIC_LABEL_PREFIX))
name = JcampConsts.SPECIFIC_LABEL_PREFIX + name;
else if (labelType === JLabelType.PRIVATE && !name.startsWith(JcampConsts.PRIVATE_LABEL_PREFIX))
name = JcampConsts.PRIVATE_LABEL_PREFIX + name;
var result = {
'labelName': name,
'labelType': labelType,
'labelCategory': labelCategory,
'dataType': dataType
};
if (specificType)
result.specificType = specificType;
Kekule.IO.Jcamp.LabelTypeInfos[name] = result;
return result;
};
Kekule.IO.Jcamp.LabelTypeInfos.createInfos = function(infoItems)
{
for (var i = 0, l = infoItems.length; i < l; ++i)
{
var item = infoItems[i];
Kekule.IO.Jcamp.LabelTypeInfos.createInfo.apply(null, item);
}
};
Kekule.IO.Jcamp.LabelTypeInfos.getType = function(labelName)
{
var info = JcampLabelTypeInfos[labelName];
return (info && info.dataType) || JcampLabelTypeInfos._DEFAULT_TYPE;
};
Kekule.IO.Jcamp.LabelTypeInfos.getCategory = function(labelName)
{
var info = JcampLabelTypeInfos[labelName];
return (info && info.labelCategory) || JLabelCategory.ANNOTATION;
};
Kekule.IO.Jcamp.LabelTypeInfos.getInfo = function(labelName, labelType)
{
var result = JcampLabelTypeInfos[labelName];
if (result && labelType && result.labelType !== labelType) // label type not match
return null;
return result;
}
var JcampLabelTypeInfos = Kekule.IO.Jcamp.LabelTypeInfos;
var _createLabelTypeInfos = Kekule.IO.Jcamp.LabelTypeInfos.createInfos;
// create type infos
_createLabelTypeInfos([
// global labels
['TITLE', JValueType.S