UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,563 lines (1,507 loc) 109 kB
/* * 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