kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,174 lines (1,136 loc) • 41.5 kB
JavaScript
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /utils/kekule.textHelper.js
* requires /core/kekule.common.js
* requires /core/kekule.metrics.js
* requires /core/kekule.structures.js
* requires /io/kekule.io.js
* requires /io/jcamp/kekule.io.jcamp.base.js
* requires /localization
*/
(function(){
"use strict";
var OU = Kekule.ObjUtils;
var AU = Kekule.ArrayUtils;
var Jcamp = Kekule.IO.Jcamp;
Kekule.globalOptions.add('IO.jcamp', {
determinateBondStereoByZIndex: true, // whether use the z-index of raster to set the stereo (wedge or hash) of single bond
enableCloserBondBetweenPositiveZIndexAtoms: true, // if true, a single bond connecting two positive z-index atom will be marked as bold
outputCsVersion: '3.7', // The CS version when writing to JCAMP-CS data
csCoordAllowedSavingErrorRatio: 0.0001, // allow 0.01% error when saving atom coords to JCAMP-CS format
csCoordPreferredScaledRange: {min: -16384, max: 16384},
autoScaleCsRasterCoords: true, // whether scale the XY raster value of CS data to a suitable length for rendering
csRasterAutoScaleRefLength: Kekule.globalOptions.structure.defaultBondLength2D || 0.8
});
/** @ignore */
Object.extend(Jcamp.Consts, {
MOL_STRUCTURE_VALUE_GROUP_DELIMITER: '\t',
MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN: /\s+/g,
MOL_ATOM_SYMBOL_ANY: 'A',
MOL_ATOM_MASS_NUMBER_PREFIX: '^',
LABEL_MOL_NAMES: 'NAMES',
LABEL_MOL_FORMULA: 'MOLFORM',
LABEL_MOL_ATOMLIST: 'ATOMLIST',
LABEL_MOL_BONDLIST: 'BONDLIST',
LABEL_MOL_CHARGELIST: 'CHARGE',
LABEL_MOL_RADICALLIST: 'RADICAL',
LABEL_MOL_RASTERLIST: 'XYRASTER',
LABEL_MOL_MAX_RASTER: 'MAXRASTER',
LABEL_MOL_COORDLIST: 'XYZ',
LABEL_MOL_COORD_FACTOR: 'XYZFACTOR',
LABEL_MOL_MAX_COORD: 'MAXXYZ',
// custom label to record the center coord/raster of molecule, based on the factor
LABEL_MOL_COORD_CENTER: Jcamp.Consts.PRIVATE_LABEL_PREFIX + 'XYZCENTER',
LABEL_MOL_RASTER_CENTER: Jcamp.Consts.PRIVATE_LABEL_PREFIX + 'XYRASTERCENTER',
LABEL_MOL_RASTER_FACTOR: Jcamp.Consts.PRIVATE_LABEL_PREFIX + 'XYRASTERFACTOR'
});
// create some ldr info for CS format
Kekule.IO.Jcamp.LabelTypeInfos.createInfos([
// IR
//['RESOLUTION', Jcamp.ValueType.STRING], // already defined in jcamp.base.js
// MS
[Jcamp.Consts.LABEL_MOL_FORMULA, Jcamp.ValueType.STRING, null, Jcamp.LabelCategory.GLOBAL],
[Jcamp.Consts.LABEL_MOL_ATOMLIST, Jcamp.ValueType.STRING, null, Jcamp.LabelCategory.GLOBAL],
[Jcamp.Consts.LABEL_MOL_BONDLIST, Jcamp.ValueType.STRING, null, Jcamp.LabelCategory.GLOBAL],
[Jcamp.Consts.LABEL_MOL_COORD_FACTOR, Jcamp.ValueType.AFFN, null, Jcamp.LabelCategory.GLOBAL],
[Jcamp.Consts.LABEL_MOL_COORD_CENTER, Jcamp.ValueType.STRING, Jcamp.LabelType.PRIVATE, Jcamp.LabelCategory.ANNOTATION],
[Jcamp.Consts.LABEL_MOL_RASTER_FACTOR, Jcamp.ValueType.AFFN, Jcamp.LabelType.PRIVATE, Jcamp.LabelCategory.ANNOTATION],
[Jcamp.Consts.LABEL_MOL_RASTER_CENTER, Jcamp.ValueType.STRING, Jcamp.LabelType.PRIVATE, Jcamp.LabelCategory.ANNOTATION],
[Jcamp.Consts.LABEL_MOL_MAX_COORD, Jcamp.ValueType.AFFN, null, Jcamp.LabelCategory.GLOBAL],
[Jcamp.Consts.LABEL_MOL_MAX_RASTER, Jcamp.ValueType.AFFN, null, Jcamp.LabelCategory.GLOBAL]
]);
/**
* A helper class for JCAMP-CS formats.
* @class
*/
Kekule.IO.Jcamp.CsUtils = {
/** @private */
getJcampIsotopeIdDetails: function(isotopeId)
{
var pattern = new RegExp('\\' + Jcamp.Consts.MOL_ATOM_MASS_NUMBER_PREFIX + '?([0-9]*)([A-z]+)');
var matchResult = isotopeId.match(pattern);
if (matchResult)
{
var massNumber = matchResult[1]? parseInt(matchResult[1]): null;
var result = {
'massNumber': massNumber,
'symbol': matchResult[2]
}
return result;
}
else
return null;
},
/**
* Convert a Jcamp isotope id (e.g. ^17O) to Kekule form (O17).
* @param {String} isotopeId
* @returns {String}
*/
jcampIsotopeIdToKekule: function(isotopeId)
{
var details = Jcamp.CsUtils.getJcampIsotopeIdDetails(isotopeId);
return details? (details.symbol + details.massNumber): isotopeId;
},
/**
* Returns a Isotope id string of JCAMP-CS.
* @param {Kekule.Isotope} isotope
* @returns {String}
*/
kekuleIsotopeToJcampIsotopeId: function(isotope)
{
var result = isotope.getSymbol();
var massNumber = isotope.getMassNumber();
if (massNumber)
result = Jcamp.Consts.MOL_ATOM_MASS_NUMBER_PREFIX + Math.round(massNumber) + result;
return result;
},
/**
* Convert a JCAMP bond type string to corresponding {@link Kekule.BondType} and {@link Kekule.BondOrder}.
* @param {String} sBondType
* @returns {Hash} {bondType, bondOrder}
*/
jcampBondTypeToKekule: function(sBondType)
{
var BT = Kekule.BondType;
var BO = Kekule.BondOrder;
var s = sBondType.toUpperCase();
var result = {};
if (s === 'A')
{
result.bondType = BT.UNKNOWN;
result.bondOrder = BO.UNSET;
}
else
{
result.bondType = BT.COVALENT;
result.bondOrder = (s === 'S')? BO.SINGLE:
(s === 'D')? BO.DOUBLE:
(s === 'T')? BO.TRIPLE:
(s === 'Q')? BO.QUAD:
BO.UNSET;
}
return result;
},
/**
* Get a JCAMP bond type string from {@link Kekule.BondType} and {@link Kekule.BondOrder}.
* @param {Int} bondType
* @param {Int} bondOrder
* @returns {String}
*/
kekuleBondTypeAndOrderToJcampBondType: function(bondType, bondOrder)
{
var BT = Kekule.BondType;
var BO = Kekule.BondOrder;
if (bondType === BT.COVALENT)
{
var result = (bondOrder === BO.QUAD)? 'Q':
(bondOrder === BO.TRIPLE)? 'T':
(bondOrder === BO.DOUBLE)? 'D':
(bondOrder === BO.SINGLE)? 'S':
'A';
return result;
}
else
{
return 'A';
}
},
// TODO: the radical conversion between Kekule and JCAMP should be rechecked?
/**
* Convert a JCAMP CS radical value to corresponding {@link Kekule.RadicalOrder}
* @param {Int} jcampRadical
* @return {Int}
*/
jcampRadicalToKekule: function(jcampRadical)
{
return Math.round(jcampRadical);
},
/**
* Convert a {@link Kekule.RadicalOrder} value to JCAMP CS radical value.
* @param {Int} kekuleRadical
* @return {Int}
*/
kekuleRadicalToJcamp: function(kekuleRadical)
{
return Math.round(kekuleRadical);
},
/**
* Calculate the suitable factor to convert float coord to integer.
* @param {Hash} coordMin
* @param {Hash} coordMax
* @param {Number} allowedErrorRatio
* @param {Hash} preferredCoordMin
* @param {Hash} preferredCoordMax
* @returns {Number}
*/
calcFactorForCoordRange: function(coordMin, coordMax, allowedErrorRatio, preferredCoordMin, preferredCoordMax)
{
var fields = ['x', 'y', 'z'];
var factorForError, factorForRange;
// phase 1, to match the allowedErrorRatio
for (var i = 0, l = fields.length; i < l; ++i)
{
var minValue = coordMin[fields[i]], maxValue = coordMax[fields[i]];
if (OU.notUnset(minValue) && OU.notUnset(maxValue))
{
var factor = Jcamp.Utils.calcNumFactorForRange(minValue, maxValue, allowedErrorRatio); //, preferredMin, preferredMax);
if (OU.isUnset(factorForError) || factorForError > factor)
factorForError = factor;
}
}
// phase 2, try to scale to preferred range
for (var i = 0, l = fields.length; i < l; ++i)
{
var minValue = coordMin[fields[i]], maxValue = coordMax[fields[i]];
var preferredMin = preferredCoordMin[fields[i]], preferredMax = preferredCoordMax[fields[i]];
var factor = Jcamp.Utils.calcNumFactorForRange(minValue, maxValue, null, preferredMin, preferredMax);
if (OU.isUnset(factorForRange) || factorForRange > factor)
factorForRange = factor;
}
return Math.min(factorForError, factorForRange);
}
};
/**
* Reader for reading a CS data block of JCAMP document tree.
* @class
* @augments Kekule.IO.Jcamp.DataBlockReader
*/
Kekule.IO.Jcamp.CsDataBlockReader = Class.create(Kekule.IO.Jcamp.DataBlockReader,
/** @lends Kekule.IO.Jcamp.CsDataBlockReader# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Jcamp.CsDataBlockReader',
/** @private */
initProperties: function()
{
this.defineProp('currAtomInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currBondInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currChargeInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currRadicalInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currCoordFactor', {'dataType': DataType.FLOAT, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currCoordCenter', {'dataType': DataType.HASH, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currCoordInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currRasterFactor', {'dataType': DataType.FLOAT, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currRasterCenter', {'dataType': DataType.HASH, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
this.defineProp('currRasterInfos', {'dataType': DataType.ARRAY, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
},
/** @ignore */
doCreateChemObjForBlock: function(block)
{
var result;
var meta = this._getBlockMeta(block);
if (meta.blockType === Jcamp.BlockType.DATA && meta.format === Jcamp.Format.CS)
{
result = new Kekule.Molecule();
}
else
result = this.tryApplySuper('doCreateChemObjForBlock', [block]);
return result;
},
/** @ignore */
doSetChemObjFromBlock: function(block, chemObj)
{
var result = this.tryApplySuper('doSetChemObjFromBlock', [block, chemObj]);
this._buildStructure(chemObj, {
atomInfos: this.getCurrAtomInfos(),
bondInfos: this.getCurrBondInfos(),
chargeInfos: this.getCurrChargeInfos(),
radicalInfos: this.getCurrRadicalInfos(),
coordFactor: this.getCurrCoordFactor(),
coordCenter: this.getCurrCoordCenter(),
coordInfos: this.getCurrCoordInfos(),
rasterFactor: this.getCurrRasterFactor(),
rasterCenter: this.getCurrRasterCenter(),
rasterInfos: this.getCurrRasterInfos()
}, this.getCurrOptions());
return result;
},
/** @ignore */
doBuildCrossRef: function(srcObj, targetObj, refType, refTypeText)
{
this.tryApplySuper('doBuildCrossRef', [srcObj, targetObj, refType, refTypeText]);
if (refType === Jcamp.CrossRefType.SPECTRUM && srcObj instanceof Kekule.StructureFragment && targetObj instanceof Kekule.Spectroscopy.Spectrum)
{
Jcamp.Utils.addMoleculeSpectrumCrossRef(targetObj, srcObj);
}
},
/** @ignore */
_initLdrHandlers: function()
{
var map = this.tryApplySuper('_initLdrHandlers');
//map[Jcamp.Consts.LABEL_BLOCK_BEGIN] = this.doStoreTitleLdr.bind(this);
// MOLFORM, ATOMLIST, BONDLIST, CHARGE
map[Jcamp.Consts.LABEL_MOL_NAMES] = this.doStoreMolNamesLdr.bind(this);
map[Jcamp.Consts.LABEL_MOL_FORMULA] = this.doStoreMolFormulaLdr.bind(this);
map[Jcamp.Consts.LABEL_MOL_ATOMLIST] = this.doStoreMolAtomListLdr.bind(this);
map[Jcamp.Consts.LABEL_MOL_BONDLIST] = this.doStoreMolBondListLdr.bind(this);
map[Jcamp.Consts.LABEL_MOL_CHARGELIST] = this.doStoreMolChargeOrRadicalListLdr.bind(this, 'charge');
map[Jcamp.Consts.LABEL_MOL_RADICALLIST] = this.doStoreMolChargeOrRadicalListLdr.bind(this, 'radical');
map[Jcamp.Consts.LABEL_MOL_COORD_FACTOR] = this.doStoreMolCoordOrRasterFactorLdr.bind(this, 'coord');
map[Jcamp.Consts.LABEL_MOL_COORD_CENTER] = this.doStoreMolCoordOrRasterCenterLdr.bind(this, 'coord');
map[Jcamp.Consts.LABEL_MOL_COORDLIST] = this.doStoreMolCoordOrRasterListLdr.bind(this, 'coord');
map[Jcamp.Consts.LABEL_MOL_RASTER_FACTOR] = this.doStoreMolCoordOrRasterFactorLdr.bind(this, 'raster');
map[Jcamp.Consts.LABEL_MOL_RASTER_CENTER] = this.doStoreMolCoordOrRasterCenterLdr.bind(this, 'raster');
map[Jcamp.Consts.LABEL_MOL_RASTERLIST] = this.doStoreMolCoordOrRasterListLdr.bind(this, 'raster');
// RADICAL, STEREOCENTER, STEREOPAIR, STEREOMOLECULE
// MAXRASTER, XYRASTER, XYZSOURCE, MAXXYZ, XYZFACTOR, XYZ
// BLOCKID
},
/** @ignore */
getIgnoredLdrNames: function()
{
return [
'MAXRASTER', 'MAXXYZ',
'STEREOCENTER', 'STEREOPAIR', 'STEREOMOLECULE' // TODO: currently all stereo are not handled in reading JCAMP-CS
];
},
/* @private */
/*
doStoreTitleLdr: function(ldr, block, chemObj, preferredInfoPropName)
{
chemObj.setInfoValue('title', Jcamp.LdrValueParserCoder.parseValue(ldr));
},
*/
/** @private */
doStoreMolNamesLdr: function(ldr, block, chemObj, preferredInfoPropName)
{
var name = ldr.valueLines[0].trim(); // TODO: now only set the name of molecule by the first name of NAMES ldr
if (chemObj.setName)
chemObj.setName(name);
},
/** @private */
doStoreMolFormulaLdr: function(ldr, block, chemObj, preferredInfoPropName)
{
/*
var formulaText = Jcamp.LdrValueParserCoder.parseValue(ldr).trim();
// actually, the formula text can be used directly in Kekule.js, just need to replace the sub/sup prefixes
formulaText = formulaText.replace(new RegExp(Jcamp.Consts.MOL_FORMULA_SUP_PREFIX, 'g'), ' ');
formulaText = formulaText.replace(new RegExp(Jcamp.Consts.MOL_FORMULA_SUB_PREFIX, 'g'), '');
var formula = Kekule.FormulaUtils.textToFormula(formulaText, chemObj);
*/
var formula = Jcamp.LdrValueParserCoder.parseValue(ldr);
if (formula && formula instanceof Kekule.MolecularFormula)
chemObj.setFormula(formula);
},
/** @private */
doStoreMolAtomListLdr: function(ldr, block, chemObj, preferredInfoPropName)
{
var atomInfos = [];
var lines = ldr.valueLines;
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i].trim();
if (line)
{
var parts = line.split(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN);
var atomIndex = parseInt(parts[0]);
var atomSymbol = (parts[1] || '').trim();
var implicitHCount = parseInt(parts[2]) || null;
if (atomSymbol && OU.notUnset(atomIndex))
{
/*
atomInfos.push({
'index': atomIndex,
'symbol': atomSymbol,
'implicitHCount': implicitHCount
});
*/
atomInfos[atomIndex] = {
'symbol': atomSymbol,
'implicitHCount': implicitHCount
};
}
}
}
//console.log('atom infos', atomInfos);
this.setPropStoreFieldValue('currAtomInfos', atomInfos);
},
/** @private */
doStoreMolBondListLdr: function(ldr, block, chemObj, preferredInfoPropName)
{
var bondInfos = [];
var lines = ldr.valueLines;
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i].trim();
if (line)
{
var parts = line.split(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN);
var atomIndex1 = parseInt(parts[0]);
var atomIndex2 = parseInt(parts[1]);
var sBondType = parts[2].trim();
if (OU.notUnset(atomIndex1) && OU.notUnset(atomIndex2) && sBondType)
{
//var bondTypeAndOrder = Jcamp.CsUtils.jcampBondTypeToKekule(sBondType);
bondInfos.push({
'atomIndex1': atomIndex1,
'atomIndex2': atomIndex2,
/*
'bondType': bondTypeAndOrder.bondType,
'bondOrder': bondTypeAndOrder.bondOrder
*/
'bondType': sBondType
});
}
}
}
this.setPropStoreFieldValue('currBondInfos', bondInfos);
},
/** @private */
doStoreMolChargeOrRadicalListLdr: function(chargeOrRadical, ldr, block, chemObj, preferredInfoPropName)
{
var infos = [];
var fieldName = chargeOrRadical;
var lines = ldr.valueLines;
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i].trim();
if (line)
{
var parts = line.split(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN);
var value = parseFloat(parts[0]);
var atomIndexes = [];
// the charge is delocalized to a set of atoms
for (var j = 1, jj = parts.length; j < jj; ++j)
{
var index = parseInt(parts[j]);
if (OU.notUnset(index) && index >= 0)
atomIndexes.push(index);
}
if (OU.notUnset(value) && atomIndexes.length)
{
var info = {'atomIndexes': atomIndexes};
info[fieldName] = value;
infos.push(info);
}
}
}
if (fieldName === 'charge')
this.setPropStoreFieldValue('currChargeInfos', infos);
else if (fieldName === 'radical')
this.setPropStoreFieldValue('currRadicalInfos', infos);
},
/** @private */
doStoreMolCoordOrRasterFactorLdr: function(fieldName, ldr, block, chemObj, preferredInfoPropName)
{
var value = Jcamp.LdrValueParserCoder.parseValue(ldr) || 1;
if (fieldName === 'coord')
this.setPropStoreFieldValue('currCoordFactor', value);
else if (fieldName === 'raster')
this.setPropStoreFieldValue('currRasterFactor', value);
},
/** @private */
doStoreMolCoordOrRasterCenterLdr: function(fieldName, ldr, block, chemObj, preferredInfoPropName)
{
var line = ldr.valueLines[0];
if (line)
{
var parts = line.split(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN);
var value = {'x': parseFloat(parts[0]), 'y': parseFloat(parts[1])}
if (parts[2])
value.z = parseFloat(parts[2]);
if (fieldName === 'coord')
this.setPropStoreFieldValue('currCoordCenter', value);
else if (fieldName === 'raster')
this.setPropStoreFieldValue('currRasterCenter', value);
}
},
/** @private */
doStoreMolCoordOrRasterListLdr: function(fieldName, ldr, block, chemObj, preferredInfoPropName)
{
var infos = [];
var lines = ldr.valueLines;
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i].trim();
if (line)
{
var parts = line.split(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER_PATTERN);
var atomIndex = parseInt(parts[0]);
var coords = [];
for (var j = 1, jj = parts.length; j < jj; ++j)
{
var value = parseFloat(parts[j]);
if (OU.notUnset(value))
coords.push(value);
}
if (OU.notUnset(atomIndex) && coords.length)
{
var info = {
'atomIndex': atomIndex,
'x': coords[0],
'y': coords[1],
'z': coords[2]
};
infos.push(info);
}
}
}
if (fieldName === 'coord')
this.setPropStoreFieldValue('currCoordInfos', infos);
else if (fieldName === 'raster')
this.setPropStoreFieldValue('currRasterInfos', infos);
},
/** @private */
_buildStructure: function(molecule, infos, options)
{
var molAtomInfos = infos.atomInfos || this.getCurrAtomInfos() || [];
var molBondInfos = infos.bondInfos || this.getCurrBondInfos() || [];
var molChargeInfos = infos.chargeInfos || this.getCurrChargeInfos() || [];
var molRadicalInfos = infos.radicalInfos || this.getCurrRadicalInfos() || [];
var molCoordInfos = infos.coordInfos || this.getCurrCoordInfos() || [];
var molCoordFactor = infos.coordFactor || this.getCurrCoordFactor() || 1;
var molCoordCenter = infos.coordCenter || this.getCurrCoordCenter();
var molRasterInfos = infos.rasterInfos || this.getCurrRasterInfos() || [];
var molRasterFactor = infos.rasterFactor || this.getCurrRasterFactor();
var molRasterCenter = infos.rasterCenter || this.getCurrRasterCenter();
var needRasterScale;
//console.log(molAtomInfos, molBondInfos);
//console.log(molRasterFactor, molRasterCenter);
molecule.beginUpdate();
try
{
// create atoms
var atoms = [];
for (var i = 0, l = molAtomInfos.length; i < l; ++i)
{
var info = molAtomInfos[i];
if (info)
{
var atom;
if (info.symbol && info.symbol !== Jcamp.Consts.MOL_ATOM_SYMBOL_ANY)
{
var isoDetails = Jcamp.CsUtils.getJcampIsotopeIdDetails(info.symbol);
atom = molecule.appendAtom(isoDetails.symbol, isoDetails.massNumber);
}
else
{
atom = new Kekule.Pseudoatom(null, Kekule.PseudoatomType.ANY);
molecule.appendNode(atom);
}
atoms[i] = atom;
}
}
// set coord2D/3D of atoms by raster and coord infos
var CU = Kekule.CoordUtils;
if (molCoordInfos.length)
{
var centerCoord = molCoordCenter || {'x': 0, 'y': 0, 'z': 0};
for (var i = 0, l = molCoordInfos.length; i < l; ++i)
{
var info = molCoordInfos[i];
if (info)
{
var atom = atoms[info.atomIndex];
if (atom)
{
var coord = CU.multiply(CU.add({'x': info.x, 'y': info.y, 'z': info.z}, centerCoord), molCoordFactor);
atom.setCoord3D(coord);
}
}
}
}
if (molRasterInfos.length)
{
needRasterScale = !molRasterFactor && options.autoScaleCsRasterCoords && options.csRasterAutoScaleRefLength;
var centerCoord = molRasterCenter || {'x': 0, 'y': 0};
for (var i = 0, l = molRasterInfos.length; i < l; ++i)
{
var info = molRasterInfos[i];
if (info)
{
var atom = atoms[info.atomIndex];
if (atom)
{
var coord = CU.multiply(CU.add({'x': info.x, 'y': info.y}, centerCoord), molRasterFactor || 1);
//coord = CU.divide(coord, 10000);
atom.setCoord2D(coord);
if (info.z) // set to zIndex2D property of atom
atom.setZIndex2D(info.z);
}
}
}
}
// create bonds
var bondLengthes = [];
for (var i = 0, l = molBondInfos.length; i < l; ++i)
{
var info = molBondInfos[i];
if (info)
{
var atom1 = atoms[info.atomIndex1];
var atom2 = atoms[info.atomIndex2];
var sBondType = info.bondType;
var bondTypeAndOrder = Jcamp.CsUtils.jcampBondTypeToKekule(sBondType);
var bond = molecule.appendBond([atom1, atom2], bondTypeAndOrder.bondOrder, bondTypeAndOrder.bondType);
if (needRasterScale)
{
var bondLength = CU.getDistance(atom1.getCoord2D(), atom2.getCoord2D());
if (bondLength)
bondLengthes.push(bondLength);
}
if (options.determinateBondStereoByZIndex)
{
if (bondTypeAndOrder.bondType === Kekule.BondType.COVALENT && bondTypeAndOrder.bondOrder === Kekule.BondOrder.SINGLE)
{
// determinate the wedge or hash bond style by z index of atom1/atom2
var zIndex1 = atom1.getZIndex2D() || 0;
var zIndex2 = atom2.getZIndex2D() || 0;
var stereo = (zIndex1 < zIndex2) ? Kekule.BondStereo.UP :
(zIndex1 > zIndex2) ? Kekule.BondStereo.DOWN :
null;
if (!stereo && zIndex1 > 0 && options.enableCloserBondBetweenPositiveZIndexAtoms) // zIndex1 == zIndex2 > 0
stereo = Kekule.BondStereo.CLOSER;
if (stereo)
bond.setStereo(stereo);
}
}
}
}
// adjust raster coords if needed
if (needRasterScale && bondLengthes.length)
{
var median = Kekule.ArrayUtils.getMedian(bondLengthes);
var rasterScale = options.csRasterAutoScaleRefLength / median;
for (var i = 0, l = atoms.length; i < l; ++i)
{
var atom = atoms[i];
if (atom)
{
var oldRaster = atom.getCoord2D();
if (oldRaster)
atom.setCoord2D(CU.multiply(oldRaster, rasterScale));
}
}
}
// map charges and radicals
for (var i = 0, l = molChargeInfos.length; i < l; ++i)
{
var info = molChargeInfos[i];
if (info && info.charge)
{
// TODO: currently the charge a averaged to each delocalization atom
var delocalizationAtomCount = info.atomIndexes.length;
if (delocalizationAtomCount)
{
var chargedAtoms = [];
for (var j = 0; j < delocalizationAtomCount; ++j)
{
var atomIndex = info.atomIndexes[j];
if (atoms[atomIndex])
chargedAtoms.push(atoms[atomIndex]);
}
var averageCharge = info.charge / chargedAtoms.length;
for (var j = 0, jj = chargedAtoms.length; j < jj; ++j)
{
chargedAtoms[j].setCharge(averageCharge);
}
}
}
}
for (var i = 0, l = molRadicalInfos.length; i < l; ++i)
{
var info = molRadicalInfos[i];
var radical = info && info.radical;
if (radical)
{
// all atoms has the same radical
for (var j = 0, jj = info.atomIndexes.length; j < jj; ++j)
{
var atom = atoms[info.atomIndexes[j]];
if (atom)
atom.setRadical(Jcamp.CsUtils.jcampRadicalToKekule(radical));
}
}
}
// use the explicit H count to validate the molecule structure, or set explicit H count
for (var i = 0, l = molAtomInfos.length; i < l; ++i)
{
var info = molAtomInfos[i];
if (info)
{
var atom = atoms[i];
var storedImplicitHCount = info.implicitHCount;
if (OU.notUnset(storedImplicitHCount))
{
var calculatedImplicitHCount = atom.getImplicitHydrogenCount();
if (storedImplicitHCount !== calculatedImplicitHCount)
{
if (atom.setExplicitHydrogenCount)
atom.setExplicitHydrogenCount(storedImplicitHCount);
else
Kekule.error(Kekule.$L('ErrorMsg.JCAMP_IMPLICIT_HYDROGEN_COUNT_NOT_MATCH_DETAIL').format(i, info.symbol, storedImplicitHCount, calculatedImplicitHCount));
}
}
}
}
// if molecule ctab been built, we shall now clear the formula of it to avoid possible insync
if (molecule.hasCtab() && molecule.hasFormula())
molecule.removeFormula();
}
finally
{
molecule.endUpdate();
}
}
});
/**
* Writer for writing a CS data block from a chem structure to JCAMP document tree.
* The input chem object should be an instance of {@link Kekule.StructureFragment}.
* @class
* @augments Kekule.IO.Jcamp.BlockWriter
*/
Kekule.IO.Jcamp.CsDataBlockWriter = Class.create(Kekule.IO.Jcamp.BlockWriter,
/** @lends Kekule.IO.Jcamp.CsDataBlockWriter# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.Jcamp.CsDataBlockWriter',
/** @private */
initProperties: function()
{
this.defineProp('atomIndexMap', {'dataType': DataType.OBJECT, 'setter': false, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
},
/** @ignore */
_initLdrCreators: function()
{
this.tryApplySuper('_initLdrCreators');
},
/** @ignore */
getTitleForBlock: function(chemObj)
{
return chemObj.getInfoValue('title') || chemObj.getName() || chemObj.getId();
},
/** @ignore */
doSaveJcampVersionToBlock: function(chemObj, block, options)
{
this.saveToLdrInBlock(block, chemObj, '', options.outputCsVersion || Kekule.globalOptions.IO.jcamp.outputCsVersion, Jcamp.Consts.LABEL_CS_VERSION);
},
/** @ignore */
doSaveChemObjToBlock: function(chemObj, block, options)
{
this.tryApplySuper('doSaveChemObjToBlock', [chemObj, block, options]);
var atomIndexMap = new Kekule.MapEx();
try
{
this.setPropStoreFieldValue('atomIndexMap', atomIndexMap);
// flatten the molecule first, since the JCAMP-CS can not save sub group
// also convert explicit aromatic bonds to single/double, since they are also unable to be saved in CS
var clonedMol = chemObj.clone(true);
try
{
if (clonedMol.kekulize())
clonedMol.kekulize();
var flattenedMol = clonedMol.getFlattenedShadowFragment();
}
finally
{
clonedMol.finalize();
}
// info
//this.doSaveMolInfoToBlock(chemObj, block, options);
// formula
this.doSaveMolFormulaToBlock(chemObj, block, options);
if (flattenedMol.hasCtab())
{
var atomLdrs = this.doGenerateMolAtomsLdrs(flattenedMol, options);
var bondLdrs = this.doGenerateMolBondsLdrs(flattenedMol, options);
// atom basic property, index and symbol, to ATOMS ldr
if (atomLdrs.atoms)
this.setLdrInBlock(block, atomLdrs.atoms);
// bond, to BONDS ldr
if (bondLdrs.bonds)
this.setLdrInBlock(block, bondLdrs.bonds);
// charge and radicals
if (atomLdrs.charges)
this.setLdrInBlock(block, atomLdrs.charges);
if (atomLdrs.radicals)
this.setLdrInBlock(block, atomLdrs.radicals);
// atom coord and raster
if (atomLdrs.rasters)
{
if (atomLdrs.maxRaster)
this.setLdrInBlock(block, atomLdrs.maxRaster);
if (atomLdrs.rasterFactor)
this.setLdrInBlock(block, atomLdrs.rasterFactor);
if (atomLdrs.rasterCenter)
this.setLdrInBlock(block, atomLdrs.rasterCenter);
this.setLdrInBlock(block, atomLdrs.rasters);
}
if (atomLdrs.coords && atomLdrs.coordFactor)
{
if (atomLdrs.maxCoord)
this.setLdrInBlock(block, atomLdrs.maxCoord);
this.setLdrInBlock(block, atomLdrs.coordFactor);
if (atomLdrs.coordCenter)
this.setLdrInBlock(block, atomLdrs.coordCenter);
this.setLdrInBlock(block, atomLdrs.coords);
}
}
}
finally
{
atomIndexMap.finalize();
}
},
/* @private */
/*
doSaveMolInfoToBlock: function(chemObj, block, options)
{
// infos
var keys = chemObj.getInfoKeys();
for (var i = 0, l = keys.length; i < l; ++i)
{
var key = keys[i];
var jsValue = chemObj.getInfoValue(key);
if (Kekule.ObjUtils.notUnset(jsValue))
{
this.doSaveMolInfoItemToBlock(chemObj, key, key, jsValue, block, options);
}
}
// generate datetime
this.saveToLdrInBlock(block, chemObj, '', new Date(), 'LONGDATE', false);
},
*/
/* @private */
/*
doSaveMolInfoItemToBlock: function(chemObj, infoKey, infoJsCascadeName, infoValue, block, options)
{
var ignoredInfoKeys = ['title', 'date'];
if (ignoredInfoKeys.indexOf(infoKey) >= 0)
return;
var ignoredLabels = [Jcamp.Consts.LABEL_BLOCK_BEGIN, Jcamp.Consts.LABEL_BLOCK_END, Jcamp.Consts.LABEL_DX_VERSION, Jcamp.Consts.LABEL_CS_VERSION];
// those labels are handled individually, do not save here
var jcampLabelName = Jcamp.Utils.kekuleLabelNameToJcamp(infoKey, null);
if (ignoredLabels.indexOf(jcampLabelName) < 0)
{
this.saveToLdrInBlock(block, chemObj, infoJsCascadeName, infoValue, jcampLabelName, false); // do not overwrite existing labels
}
},
*/
/** @private */
doSaveMolFormulaToBlock: function(chemObj, block, options)
{
var formula = chemObj.calcFormula();
/*
var sections = AU.clone(formula.getSections());
// sort
var getSortIndexes = function(formulaSection)
{
var primary = 'ZZZZZ', secondary = 0;
var atom = formulaSection.obj;
if (atom)
{
if (atom.getSymbol)
primary = atom.getSymbol();
// C/H will be put to head of seq
if (primary === 'C')
primary = '0';
else if (primary === 'H')
primary = '1';
if (atom.getMassNumber)
secondary = atom.getMassNumber() || 0;
}
return [primary, secondary];
};
sections.sort(function(sec1, sec2){
var i1 = getSortIndexes(sec1);
var i2 = getSortIndexes(sec2);
return AU.compare(i1, i2);
});
var outputItems = [];
for (var i = 0, l = sections.length; i < l; ++i)
{
var sec = sections[i];
var atom = sec.obj;
var count = sec.count;
var symbol = atom.getLabel && atom.getLabel();
if (atom.getMassNumber && atom.getMassNumber())
symbol = Jcamp.Consts.MOL_FORMULA_SUP_PREFIX + symbol;
if (count > 1)
symbol += count;
outputItems.push(symbol);
}
var valueLine = outputItems.join(' ');
*/
if (formula)
this.saveToLdrInBlock(block, chemObj, '', formula, Jcamp.Consts.LABEL_MOL_FORMULA);
},
/** @private */
doGenerateMolAtomsLdrs: function(mol, options)
{
var atomIndexMap = this.getAtomIndexMap();
var atomListLdr, atomChargeLdr, atomRadicalLdr,
atomCoordLdr, atomCoordFactorLdr, atomCoordCenterLdr, atomRasterLdr, atomRasterFactorLdr, atomRasterCenterLdr,
atomMaxCoordLdr, atomMaxRasterLdr;
var currIndex = 1;
var atomListValueLines = [''];
var atomCoordLines = [''];
var atomRasterLines = [''];
var chargeItems = [];
var radicalMap = [];
var coords = [];
var rasters = [];
var getCoord = function(node, coordMode)
{
if (node.hasCoordOfMode(coordMode))
{
var method = node.getAbsCoordOfMode || node.getCoordOfMode;
return method.apply(node, [coordMode]);
}
else
return null;
}
var hasCoord3D, hasCoord2D;
for (var i = 0, l = mol.getNodeCount(); i < l; ++i)
{
var node = mol.getNodeAt(i);
if (node instanceof Kekule.ChemStructureNode)
{
var symbol;
var isotope = node.getPrimaryIsotope && node.getPrimaryIsotope();
if (isotope)
symbol = Jcamp.CsUtils.kekuleIsotopeToJcampIsotopeId(isotope);
else
symbol = Jcamp.Consts.MOL_ATOM_SYMBOL_ANY; // TODO: does pseudo atom is allowed in CS?
var hCount = node.getHydrogenCount && node.getHydrogenCount(false); //node.getImplicitHydrogenCount && node.getImplicitHydrogenCount();
var parts = [currIndex, symbol];
if (hCount)
parts.push(hCount);
atomListValueLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
// coord3D
//if (hasCoord3D)
{
var coord3D = getCoord(node, Kekule.CoordMode.COORD3D);
if (coord3D)
hasCoord3D = true;
else
coord3D = {'x': 0, 'y': 0, 'z': 0};
coord3D._index = currIndex;
coords.push(coord3D);
/*
parts = [currIndex, coord3D.x || 0, coord3D.y || 0, coord3D.z || 0];
atomCoordLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
*/
}
// coord2D
//if (hasCoord2D)
{
var coord2D = getCoord(node, Kekule.CoordMode.COORD2D);
if (coord2D)
hasCoord2D = true;
else
coord2D = {'x': 0, 'y': 0};
coord2D._index = currIndex;
var zIndex = node.getZIndex2D && node.getZIndex2D();
if (zIndex)
coord2D.zIndex = zIndex;
rasters.push(coord2D);
/*
atomRasterLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
*/
}
// charge
var charge = node.getCharge && node.getCharge();
if (charge)
chargeItems.push({'charge': Math.round(charge), 'atomIndex': currIndex});
// radical
var radical = node.getRadical && node.getRadical();
if (radical)
{
radical = Jcamp.CsUtils.kekuleRadicalToJcamp(radical);
if (!radicalMap[radical])
radicalMap[radical] = [currIndex];
else
radicalMap[radical].push(currIndex);
}
atomIndexMap.set(node, currIndex);
++currIndex;
}
}
// create atom list LDR
if (atomListValueLines.length > 1) // really has atoms
atomListLdr = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_ATOMLIST, atomListValueLines);
// create atom charge list lDR
if (chargeItems.length)
{
var valueLines = [''];
for (var i = 0, l = chargeItems.length; i < l; ++i)
{
var item = chargeItems[i];
if (item)
{
var sCharge = (item.charge > 0) ? '+' + item.charge : item.charge.toString();
var parts = [sCharge, item.atomIndex];
valueLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
}
}
atomChargeLdr = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_CHARGELIST, valueLines);
}
// create atom radical LDR
if (radicalMap.length)
{
var valueLines = [''];
for (var i = 0, l = radicalMap.length; i < l; ++i)
{
var item = radicalMap[i];
if (item)
{
var parts = [i]; // radical number
parts = parts.concat(radicalMap[i]); // atom indexes
valueLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
}
}
if (valueLines.length > 1)
atomRadicalLdr = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_RADICALLIST, valueLines);
}
// create coord/raster LDR, note we need to convert the coords into integer
var CU = Kekule.CoordUtils;
var atomCoordSets = [hasCoord2D? rasters: [], hasCoord3D? coords: []]; // 2D and 3D coords
for (var i = 0, l = atomCoordSets.length; i < l; ++i)
{
var maxCoordValue;
var atomCoords = atomCoordSets[i];
var valueLines = [''];
var fields = (i === 0)? ['x', 'y']: ['x', 'y', 'z'];
if (atomCoords.length)
{
// determinate the factor to convert all coords to integer
/*
var containerBox = CU.getContainerBox(atomCoords);
var delta = {'x': containerBox.x2 - containerBox.x1, 'y': containerBox.y2 - containerBox.y1, 'z': (containerBox.z2 - containerBox.z1) || 0};
var distance = CU.getDistance(delta, {'x': 0, 'y': 0, 'z': 0});
var factor = 1 / options.csCoordAllowedSavingErrorRatio;
var decimalPos = Kekule.NumUtils.getDecimalPointPos(distance);
if (decimalPos < 0)
{
factor *= Math.pow(10, -decimalPos);
}
*/
var centerCoord = CU.getCenter(atomCoords);
var cornerCoords = CU.getContainerBoxCorners(atomCoords);
cornerCoords.min = CU.substract(cornerCoords.min, centerCoord);
cornerCoords.max = CU.substract(cornerCoords.max, centerCoord);
var preferredCoordMin = {}, preferredCoordMax = {};
preferredCoordMin.x = preferredCoordMin.y = preferredCoordMin.z = options.csCoordPreferredScaledRange.min;
preferredCoordMax.x = preferredCoordMax.y = preferredCoordMax.z = options.csCoordPreferredScaledRange.max;
var factor = Jcamp.CsUtils.calcFactorForCoordRange(cornerCoords.min, cornerCoords.max, options.csCoordAllowedSavingErrorRatio, preferredCoordMin, preferredCoordMax);
//console.log(cornerCoords, centerCoord, factor);
// convert coord values
for (var j = 0, jj = atomCoords.length; j < jj; ++j)
{
var parts = [atomCoords[j]._index]; // atom index
var coord = CU.divide(CU.substract(atomCoords[j], centerCoord), factor);
for (var k = 0, kk = fields.length; k < kk; ++k)
{
var field = fields[k];
var v = Math.round(coord[field] || 0);
parts.push(v);
if (Kekule.ObjUtils.isUnset(maxCoordValue) || v > maxCoordValue)
maxCoordValue = v;
}
if (atomCoords[j].zIndex) // zIndex in coord2D
parts.push(atomCoords[j].zIndex);
valueLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
}
// center coord
var coord = CU.divide(centerCoord, factor);
var parts = [];
for (var k = 0, kk = fields.length; k < kk; ++k)
{
var field = fields[k];
parts.push(Math.round(coord[field] || 0));
}
var centerCoordValueLine = parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER);
if (valueLines.length > 1)
{
if (i === 0) // raster
{
atomRasterLdr = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_RASTERLIST, valueLines);
atomRasterFactorLdr = this.createLdr('', factor, Jcamp.Consts.LABEL_MOL_RASTER_FACTOR);
atomRasterCenterLdr = this.createLdr('', centerCoordValueLine, Jcamp.Consts.LABEL_MOL_RASTER_CENTER);
atomMaxRasterLdr = this.createLdr('', maxCoordValue, Jcamp.Consts.LABEL_MOL_MAX_RASTER);
}
else
{
atomCoordLdr = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_COORDLIST, valueLines);
atomCoordFactorLdr = this.createLdr('', factor, Jcamp.Consts.LABEL_MOL_COORD_FACTOR);
atomCoordCenterLdr = this.createLdr('', centerCoordValueLine, Jcamp.Consts.LABEL_MOL_COORD_CENTER);
atomMaxCoordLdr = this.createLdr('', maxCoordValue, Jcamp.Consts.LABEL_MOL_MAX_COORD);
}
}
}
}
return {
'atoms': atomListLdr,
'charges': atomChargeLdr,
'radicals': atomRadicalLdr,
'rasters': atomRasterLdr,
'rasterFactor': atomRasterFactorLdr,
'rasterCenter': atomRasterCenterLdr,
'coords': atomCoordLdr,
'coordFactor': atomCoordFactorLdr,
'coordCenter': atomCoordCenterLdr,
'maxCoord': atomMaxCoordLdr,
'maxRaster': atomMaxRasterLdr
}
},
/** @private */
doGenerateMolBondsLdrs: function(mol, options)
{
var atomIndexMap = this.getAtomIndexMap();
var valueLines = [''];
for (var i = 0, l = mol.getConnectorCount(); i < l; ++i)
{
var connector = mol.getConnectorAt(i);
if (connector instanceof Kekule.Bond)
{
var parts = [];
var nodes = connector.getConnectedChemNodes();
for (var j = 0, jj = nodes.length; j < jj; ++j)
{
var index = atomIndexMap.get(nodes[j]);
if (OU.notUnset(index))
{
parts.push(index);
if (parts.length >= 2) // CS format can only handles bond connecting two atoms
break;
}
}
var bondType = Jcamp.CsUtils.kekuleBondTypeAndOrderToJcampBondType(connector.getBondType(), connector.getBondOrder());
parts.push(bondType);
valueLines.push(parts.join(Jcamp.Consts.MOL_STRUCTURE_VALUE_GROUP_DELIMITER));
}
}
var result = {};
if (valueLines.length > 1) // has real bonds
{
result.bonds = this.createLdrRaw(Jcamp.Consts.LABEL_MOL_BONDLIST, valueLines);
}
return result;
}
});
// register
Jcamp.BlockReaderManager.register(Jcamp.BlockType.DATA, Jcamp.Format.CS, Kekule.IO.Jcamp.CsDataBlockReader);
Jcamp.BlockWriterManager.register(Kekule.StructureFragment, Kekule.IO.Jcamp.CsDataBlockWriter);
})();