Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,110 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var generics = {
atom: {
'any': {
labels: ['A', 'AH']
},
'no-carbon': {
labels: ['Q', 'QH']
},
'metal': {
labels: ['M', 'MH']
},
'halogen': {
labels: ['X', 'XH']
}
},
group: {
labels: ['G', 'GH', 'G*', 'GH*'],
acyclic: {
labels: ['ACY', 'ACH'],
carbo: {
labels: ['ABC', 'ABH'],
alkynyl: {
labels: ['AYL', 'AYH']
},
alkyl: {
labels: ['ALK', 'ALH']
},
alkenyl: {
labels: ['AEL', 'AEH']
}
},
hetero: {
labels: ['AHC', 'AHH'],
alkoxy: {
labels: ['AOX', 'AOH']
}
}
},
cyclic: {
'labels': ['CYC', 'CYH'],
'no-carbon': {
labels: ['CXX', 'CXH']
},
'carbo': {
labels: ['CBC', 'CBH'],
aryl: {
labels: ['ARY', 'ARH']
},
cycloalkyl: {
labels: ['CAL', 'CAH']
},
cycloalkenyl: {
labels: ['CEL', 'CEH']
}
},
'hetero': {
labels: ['CHC', 'CHH'],
aryl: {
labels: ['HAR', 'HAH']
}
}
}
},
special: {
labels: ['H+', 'D', 'T', 'R', 'Pol']
}
};
function mapify(tree, path, res) {
return Object.keys(tree).reduce(function (res, key) {
if (key === 'labels') {
return tree.labels.reduce(function (res, label) {
res[label] = path || true;
return res;
}, res);
}
return mapify(tree[key],
path ? path.concat(key) : [key], res);
}, res || {});
}
function traverse(tree, path) {
return path.reduce(function (res, cur) {
return (res && res[cur]) || null;
}, tree);
}
generics.map = mapify(generics);
generics.map['*'] = generics.map['A']; // alias
generics.get = function (path) {
return mapify(traverse(path));
};
module.exports = generics;

View File

@ -0,0 +1,269 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Set = require('../../util/set');
var v2000 = require('./v2000');
var v3000 = require('./v3000');
var Struct = require('./../struct/index');
var utils = require('./utils');
var loadRGroupFragments = true; // TODO: set to load the fragments
/* Parse Mol */
function parseMol(/* string */ ctabLines) /* Struct */ {
/* reader */
if (ctabLines[0].search('\\$MDL') == 0)
return v2000.parseRg2000(ctabLines);
var struct = parseCTab(ctabLines.slice(3));
struct.name = ctabLines[0].trim();
return struct;
}
function parseCTab(/* string */ ctabLines) /* Struct */ {
/* reader */
var countsSplit = partitionLine(ctabLines[0], utils.fmtInfo.countsLinePartition);
var version = countsSplit[11].trim();
ctabLines = ctabLines.slice(1);
if (version == 'V2000')
return v2000.parseCTabV2000(ctabLines, countsSplit);
else if (version == 'V3000')
return v3000.parseCTabV3000(ctabLines, !loadRGroupFragments);
else
throw new Error('Molfile version unknown: ' + version); // eslint-disable-line no-else-return
}
/* Parse Rxn */
function parseRxn(/* string[] */ ctabLines) /* Struct */ {
/* reader */
var split = ctabLines[0].trim().split(' ');
if (split.length > 1 && split[1] == 'V3000')
return v3000.parseRxn3000(ctabLines);
else
return v2000.parseRxn2000(ctabLines); // eslint-disable-line no-else-return
}
/* Prepare For Saving */
var prepareForSaving = {
MUL: Struct.SGroup.prepareMulForSaving,
SRU: prepareSruForSaving,
SUP: prepareSupForSaving,
DAT: prepareDatForSaving,
GEN: prepareGenForSaving
};
function prepareSruForSaving(sgroup, mol) {
var xBonds = [];
mol.bonds.each(function (bid, bond) {
var a1 = mol.atoms.get(bond.begin);
var a2 = mol.atoms.get(bond.end);
/* eslint-disable no-mixed-operators*/
if (Set.contains(a1.sgs, sgroup.id) && !Set.contains(a2.sgs, sgroup.id) ||
Set.contains(a2.sgs, sgroup.id) && !Set.contains(a1.sgs, sgroup.id))
/* eslint-enable no-mixed-operators*/
xBonds.push(bid);
}, sgroup);
if (xBonds.length != 0 && xBonds.length != 2)
throw { 'id': sgroup.id, 'error-type': 'cross-bond-number', 'message': 'Unsupported cross-bonds number' };
sgroup.bonds = xBonds;
}
function prepareSupForSaving(sgroup, mol) {
// This code is also used for GroupSru and should be moved into a separate common method
// It seems that such code should be used for any sgroup by this this should be checked
var xBonds = [];
mol.bonds.each(function (bid, bond) {
var a1 = mol.atoms.get(bond.begin);
var a2 = mol.atoms.get(bond.end);
/* eslint-disable no-mixed-operators*/
if (Set.contains(a1.sgs, sgroup.id) && !Set.contains(a2.sgs, sgroup.id) ||
Set.contains(a2.sgs, sgroup.id) && !Set.contains(a1.sgs, sgroup.id))
/* eslint-enable no-mixed-operators*/
xBonds.push(bid);
}, sgroup);
sgroup.bonds = xBonds;
}
function prepareGenForSaving(sgroup, mol) { // eslint-disable-line no-unused-vars
}
function prepareDatForSaving(sgroup, mol) {
sgroup.atoms = Struct.SGroup.getAtoms(mol, sgroup);
}
/* Save To Molfile */
var saveToMolfile = {
MUL: saveMulToMolfile,
SRU: saveSruToMolfile,
SUP: saveSupToMolfile,
DAT: saveDatToMolfile,
GEN: saveGenToMolfile
};
function saveMulToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, Object.keys(sgroup.atomSet), atomMap)); // TODO: check atomSet
lines = lines.concat(makeAtomBondLines('SPA', idstr, Object.keys(sgroup.parentAtomSet), atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
var smtLine = 'M SMT ' + idstr + ' ' + sgroup.data.mul;
lines.push(smtLine);
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function saveSruToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function saveSupToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
if (sgroup.data.name && sgroup.data.name != '')
lines.push('M SMT ' + idstr + ' ' + sgroup.data.name);
return lines.join('\n');
}
function saveDatToMolfile(sgroup, mol, sgMap, atomMap) {
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var data = sgroup.data;
var pp = sgroup.pp;
if (!data.absolute)
pp = pp.sub(Struct.SGroup.getMassCentre(mol, sgroup.atoms));
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
var sdtLine = 'M SDT ' + idstr + ' ' +
(data.fieldName || '').padEnd(30) +
(data.fieldType || '').padStart(2) +
(data.units || '').padEnd(20) +
(data.query || '').padStart(2);
if (data.queryOp) // see gitlab #184
sdtLine += data.queryOp.padEnd(80 - 65);
lines.push(sdtLine);
var sddLine = 'M SDD ' + idstr +
' ' + utils.paddedNum(pp.x, 10, 4) + utils.paddedNum(-pp.y, 10, 4) +
' ' + // ' eee'
(data.attached ? 'A' : 'D') + // f
(data.absolute ? 'A' : 'R') + // g
(data.showUnits ? 'U' : ' ') + // h
' ' + // i
(data.nCharnCharsToDisplay >= 0 ? utils.paddedNum(data.nCharnCharsToDisplay, 3) : 'ALL') + // jjj
' 1 ' + // 'kkk ll '
(data.tagChar || ' ') + // m
' ' + utils.paddedNum(data.daspPos, 1) + // n
' '; // oo
lines.push(sddLine);
var val = normalizeNewlines(data.fieldValue).replace(/\n*$/, '');
var charsPerLine = 69;
val.split('\n').forEach(function (chars) {
while (chars.length > charsPerLine) {
lines.push('M SCD ' + idstr + ' ' + chars.slice(0, charsPerLine));
chars = chars.slice(charsPerLine);
}
lines.push('M SED ' + idstr + ' ' + chars);
});
return lines.join('\n');
}
function saveGenToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function makeAtomBondLines(prefix, idstr, ids, map) {
if (!ids)
return [];
var lines = [];
for (var i = 0; i < Math.floor((ids.length + 14) / 15); ++i) {
var rem = Math.min(ids.length - 15 * i, 15); // eslint-disable-line no-mixed-operators
var salLine = 'M ' + prefix + ' ' + idstr + ' ' + utils.paddedNum(rem, 2);
for (var j = 0; j < rem; ++j)
salLine += ' ' + utils.paddedNum(map[ids[i * 15 + j]], 3); // eslint-disable-line no-mixed-operators
lines.push(salLine);
}
return lines;
}
function bracketsToMolfile(mol, sg, idstr) { // eslint-disable-line max-statements
var inBonds = [];
var xBonds = [];
var atomSet = Set.fromList(sg.atoms);
Struct.SGroup.getCrossBonds(inBonds, xBonds, mol, atomSet);
Struct.SGroup.bracketPos(sg, mol, xBonds);
var bb = sg.bracketBox;
var d = sg.bracketDir;
var n = d.rotateSC(1, 0);
var brackets = Struct.SGroup.getBracketParameters(mol, xBonds, atomSet, bb, d, n);
var lines = [];
for (var i = 0; i < brackets.length; ++i) {
var bracket = brackets[i];
var a0 = bracket.c.addScaled(bracket.n, -0.5 * bracket.h).yComplement();
var a1 = bracket.c.addScaled(bracket.n, 0.5 * bracket.h).yComplement();
var line = 'M SDI ' + idstr + utils.paddedNum(4, 3);
var coord = [a0.x, a0.y, a1.x, a1.y];
for (var j = 0; j < coord.length; ++j)
line += utils.paddedNum(coord[j], 10, 4);
lines.push(line);
}
return lines;
}
// According Unicode Consortium sould be
// nlRe = /\r\n|[\n\v\f\r\x85\u2028\u2029]/g;
// http://www.unicode.org/reports/tr18/#Line_Boundaries
var nlRe = /\r\n|[\n\r]/g;
function normalizeNewlines(str) {
return str.replace(nlRe, '\n');
}
function partitionLine(/* string*/ str, /* array of int*/ parts, /* bool*/ withspace) {
/* reader */
var res = [];
for (var i = 0, shift = 0; i < parts.length; ++i) {
res.push(str.slice(shift, shift + parts[i]));
if (withspace)
shift++;
shift += parts[i];
}
return res;
}
module.exports = {
parseCTab: parseCTab,
parseMol: parseMol,
parseRxn: parseRxn,
prepareForSaving: prepareForSaving,
saveToMolfile: saveToMolfile
};

View File

@ -0,0 +1,54 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Molfile = require('./molfile');
// TODO: reconstruct molfile string instead parsing multiple times
// merge to bottom
function parseCTFile(str, options) {
var molfile = new Molfile();
var lines = str.split(/\r\n|[\n\r]/g);
try {
return molfile.parseCTFile(lines);
} catch (ex) {
if (options.badHeaderRecover) {
try {
// check whether there's an extra empty line on top
// this often happens when molfile text is pasted into the dialog window
return molfile.parseCTFile(lines.slice(1));
} catch (ex1) { //
}
try {
// check for a missing first line
// this sometimes happens when pasting
return molfile.parseCTFile([''].concat(lines));
} catch (ex2) { //
}
}
throw ex;
}
}
module.exports = {
stringify: function (struct, options) {
var opts = options || {};
return new Molfile(opts.v3000).saveMolecule(struct, opts.ignoreErrors,
opts.noRgroups, opts.preserveIndigoDesc);
},
parse: function (str, options) {
return parseCTFile(str, options || {});
}
};

View File

@ -0,0 +1,488 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var element = require('./../element');
var common = require('./common');
var utils = require('./utils');
function Molfile(v3000) {
/* reader */
/* saver */
this.molecule = null;
this.molfile = null;
this.v3000 = v3000 || false;
}
Molfile.prototype.parseCTFile = function (molfileLines) {
var ret = null;
if (molfileLines[0].search('\\$RXN') == 0)
ret = common.parseRxn(molfileLines);
else
ret = common.parseMol(molfileLines);
ret.initHalfBonds();
ret.initNeighbors();
ret.markFragments();
return ret;
};
Molfile.prototype.prepareSGroups = function (skipErrors, preserveIndigoDesc) {
var mol = this.molecule;
var toRemove = [];
var errors = 0;
this.molecule.sGroupForest.getSGroupsBFS().reverse().forEach(function (id) {
var sgroup = mol.sgroups.get(id);
var errorIgnore = false;
try {
common.prepareForSaving[sgroup.type](sgroup, mol);
} catch (ex) {
if (!skipErrors || typeof (ex.id) != 'number')
throw ex;
errorIgnore = true;
}
/* eslint-disable no-mixed-operators*/
if (errorIgnore ||
!preserveIndigoDesc && /^INDIGO_.+_DESC$/i.test(sgroup.data.fieldName)) {
/* eslint-enable no-mixed-operators*/
errors += errorIgnore;
toRemove.push(sgroup.id);
}
}, this);
if (errors)
throw new Error('WARNING: ' + errors + ' invalid S-groups were detected. They will be omitted.');
for (var i = 0; i < toRemove.length; ++i)
mol.sGroupDelete(toRemove[i]);
return mol;
};
Molfile.prototype.getCTab = function (molecule, rgroups) {
/* saver */
this.molecule = molecule.clone();
this.molfile = '';
this.writeCTab2000(rgroups);
return this.molfile;
};
Molfile.prototype.saveMolecule = function (molecule, skipSGroupErrors, norgroups, preserveIndigoDesc) { // eslint-disable-line max-statements
/* saver */
this.reaction = molecule.rxnArrows.count() > 0;
if (molecule.rxnArrows.count() > 1)
throw new Error('Reaction may not contain more than one arrow');
this.molfile = '' + molecule.name;
if (this.reaction) {
if (molecule.rgroups.count() > 0)
throw new Error('Unable to save the structure - reactions with r-groups are not supported at the moment');
var components = molecule.getComponents();
var reactants = components.reactants;
var products = components.products;
var all = reactants.concat(products);
this.molfile = '$RXN\n\n\n\n' +
utils.paddedNum(reactants.length, 3) +
utils.paddedNum(products.length, 3) +
utils.paddedNum(0, 3) + '\n';
for (var i = 0; i < all.length; ++i) {
var saver = new Molfile(false);
var submol = molecule.clone(all[i], null, true);
var molfile = saver.saveMolecule(submol, false, true);
this.molfile += '$MOL\n' + molfile;
}
return this.molfile;
}
if (molecule.rgroups.count() > 0) {
if (norgroups) {
molecule = molecule.getScaffold();
} else {
var scaffold = new Molfile(false).getCTab(molecule.getScaffold(), molecule.rgroups);
this.molfile = '$MDL REV 1\n$MOL\n$HDR\n\n\n\n$END HDR\n';
this.molfile += '$CTAB\n' + scaffold + '$END CTAB\n';
molecule.rgroups.each(function (rgid, rg) {
this.molfile += '$RGP\n';
this.writePaddedNumber(rgid, 3);
this.molfile += '\n';
rg.frags.each(function (fnum, fid) {
var group = new Molfile(false).getCTab(molecule.getFragment(fid));
this.molfile += '$CTAB\n' + group + '$END CTAB\n';
}, this);
this.molfile += '$END RGP\n';
}, this);
this.molfile += '$END MOL\n';
return this.molfile;
}
}
this.molecule = molecule.clone();
this.prepareSGroups(skipSGroupErrors, preserveIndigoDesc);
this.writeHeader();
// TODO: saving to V3000
this.writeCTab2000();
return this.molfile;
};
Molfile.prototype.writeHeader = function () {
/* saver */
var date = new Date();
this.writeCR(); // TODO: write structure name
this.writeWhiteSpace(2);
this.write('Ketcher');
this.writeWhiteSpace();
this.writeCR(((date.getMonth() + 1) + '').padStart(2) + (date.getDate() + '').padStart(2) + ((date.getFullYear() % 100) + '').padStart(2) +
(date.getHours() + '').padStart(2) + (date.getMinutes() + '').padStart(2) + '2D 1 1.00000 0.00000 0');
this.writeCR();
};
Molfile.prototype.write = function (str) {
/* saver */
this.molfile += str;
};
Molfile.prototype.writeCR = function (str) {
/* saver */
if (arguments.length == 0)
str = '';
this.molfile += str + '\n';
};
Molfile.prototype.writeWhiteSpace = function (length) {
/* saver */
if (arguments.length == 0)
length = 1;
this.write(' '.repeat(Math.max(length, 0)));
};
Molfile.prototype.writePadded = function (str, width) {
/* saver */
this.write(str);
this.writeWhiteSpace(width - str.length);
};
Molfile.prototype.writePaddedNumber = function (number, width) {
/* saver */
var str = (number - 0).toString();
this.writeWhiteSpace(width - str.length);
this.write(str);
};
Molfile.prototype.writePaddedFloat = function (number, width, precision) {
/* saver */
this.write(utils.paddedNum(number, width, precision));
};
Molfile.prototype.writeCTab2000Header = function () {
/* saver */
this.writePaddedNumber(this.molecule.atoms.count(), 3);
this.writePaddedNumber(this.molecule.bonds.count(), 3);
this.writePaddedNumber(0, 3);
this.writeWhiteSpace(3);
this.writePaddedNumber(this.molecule.isChiral ? 1 : 0, 3);
this.writePaddedNumber(0, 3);
this.writeWhiteSpace(12);
this.writePaddedNumber(999, 3);
this.writeCR(' V2000');
};
Molfile.prototype.writeCTab2000 = function (rgroups) { // eslint-disable-line max-statements
/* saver */
this.writeCTab2000Header();
this.mapping = {};
var i = 1;
/* eslint-disable camelcase*/
var atomList_list = [];
var atomProps_list = [];
/* eslint-enable camel-case*/
this.molecule.atoms.each(function (id, atom) { // eslint-disable-line max-statements
this.writePaddedFloat(atom.pp.x, 10, 4);
this.writePaddedFloat(-atom.pp.y, 10, 4);
this.writePaddedFloat(atom.pp.z, 10, 4);
this.writeWhiteSpace();
var label = atom.label;
if (atom.atomList != null) {
label = 'L';
atomList_list.push(id);
} else if (atom['pseudo']) {
if (atom['pseudo'].length > 3) {
label = 'A';
atomProps_list.push({ id: id, value: "'" + atom['pseudo'] + "'" });
}
} else if (atom['alias']) {
atomProps_list.push({ id: id, value: atom['alias'] });
} else if (!element.map[label] && ['A', 'Q', 'X', '*', 'R#'].indexOf(label) == -1) { // search in generics?
label = 'C';
atomProps_list.push({ id: id, value: atom.label });
}
this.writePadded(label, 3);
this.writePaddedNumber(0, 2);
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
if (typeof atom.hCount === "undefined")
atom.hCount = 0;
this.writePaddedNumber(atom.hCount, 3);
if (typeof atom.stereoCare === "undefined")
atom.stereoCare = 0;
this.writePaddedNumber(atom.stereoCare, 3);
this.writePaddedNumber(atom.explicitValence < 0 ? 0 : (atom.explicitValence == 0 ? 15 : atom.explicitValence), 3); // eslint-disable-line no-nested-ternary
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
if (typeof atom.aam === "undefined")
atom.aam = 0;
this.writePaddedNumber(atom.aam, 3);
if (typeof atom.invRet === "undefined")
atom.invRet = 0;
this.writePaddedNumber(atom.invRet, 3);
if (typeof atom.exactChangeFlag === "undefined")
atom.exactChangeFlag = 0;
this.writePaddedNumber(atom.exactChangeFlag, 3);
this.writeCR();
this.mapping[id] = i;
i++;
}, this);
this.bondMapping = {};
i = 1;
this.molecule.bonds.each(function (id, bond) {
this.bondMapping[id] = i++;
this.writePaddedNumber(this.mapping[bond.begin], 3);
this.writePaddedNumber(this.mapping[bond.end], 3);
this.writePaddedNumber(bond.type, 3);
if (typeof bond.stereo === "undefined")
bond.stereo = 0;
this.writePaddedNumber(bond.stereo, 3);
this.writePadded(bond.xxx, 3);
if (typeof bond.topology === "undefined")
bond.topology = 0;
this.writePaddedNumber(bond.topology, 3);
if (typeof bond.reactingCenterStatus === "undefined")
bond.reactingCenterStatus = 0;
this.writePaddedNumber(bond.reactingCenterStatus, 3);
this.writeCR();
}, this);
while (atomProps_list.length > 0) {
this.write('A ');
this.writePaddedNumber(atomProps_list[0].id + 1, 3);
this.writeCR();
this.writeCR(atomProps_list[0].value);
atomProps_list.splice(0, 1);
}
var chargeList = [];
var isotopeList = [];
var radicalList = [];
var rglabelList = [];
var rglogicList = [];
var aplabelList = [];
var rbcountList = [];
var unsaturatedList = [];
var substcountList = [];
this.molecule.atoms.each(function (id, atom) {
if (atom.charge != 0)
chargeList.push([id, atom.charge]);
if (atom.isotope != 0)
isotopeList.push([id, atom.isotope]);
if (atom.radical != 0)
radicalList.push([id, atom.radical]);
if (atom.rglabel != null && atom.label == 'R#') { // TODO need to force rglabel=null when label is not 'R#'
for (var rgi = 0; rgi < 32; rgi++)
if (atom.rglabel & (1 << rgi)) rglabelList.push([id, rgi + 1]);
}
if (atom.attpnt != null)
aplabelList.push([id, atom.attpnt]);
if (atom.ringBondCount != 0)
rbcountList.push([id, atom.ringBondCount]);
if (atom.substitutionCount != 0)
substcountList.push([id, atom.substitutionCount]);
if (atom.unsaturatedAtom != 0)
unsaturatedList.push([id, atom.unsaturatedAtom]);
});
if (rgroups) {
rgroups.each(function (rgid, rg) {
if (rg.resth || rg.ifthen > 0 || rg.range.length > 0) {
var line = ' 1 ' +
utils.paddedNum(rgid, 3) + ' ' +
utils.paddedNum(rg.ifthen, 3) + ' ' +
utils.paddedNum(rg.resth ? 1 : 0, 3) + ' ' + rg.range;
rglogicList.push(line);
}
});
}
function writeAtomPropList(propId, values) {
while (values.length > 0) {
var part = [];
while (values.length > 0 && part.length < 8) {
part.push(values[0]);
values.splice(0, 1);
}
this.write(propId);
this.writePaddedNumber(part.length, 3);
part.forEach(function (value) {
this.writeWhiteSpace();
this.writePaddedNumber(this.mapping[value[0]], 3);
this.writeWhiteSpace();
this.writePaddedNumber(value[1], 3);
}, this);
this.writeCR();
}
}
writeAtomPropList.call(this, 'M CHG', chargeList);
writeAtomPropList.call(this, 'M ISO', isotopeList);
writeAtomPropList.call(this, 'M RAD', radicalList);
writeAtomPropList.call(this, 'M RGP', rglabelList);
for (var j = 0; j < rglogicList.length; ++j)
this.write('M LOG' + rglogicList[j] + '\n');
writeAtomPropList.call(this, 'M APO', aplabelList);
writeAtomPropList.call(this, 'M RBC', rbcountList);
writeAtomPropList.call(this, 'M SUB', substcountList);
writeAtomPropList.call(this, 'M UNS', unsaturatedList);
if (atomList_list.length > 0) {
for (j = 0; j < atomList_list.length; ++j) {
var aid = atomList_list[j];
var atomList = this.molecule.atoms.get(aid).atomList;
this.write('M ALS');
this.writePaddedNumber(aid + 1, 4);
this.writePaddedNumber(atomList.ids.length, 3);
this.writeWhiteSpace();
this.write(atomList.notList ? 'T' : 'F');
var labelList = atomList.labelList();
for (var k = 0; k < labelList.length; ++k) {
this.writeWhiteSpace();
this.writePadded(labelList[k], 3);
}
this.writeCR();
}
}
var sgmap = {};
var cnt = 1;
var sgmapback = {};
var sgorder = this.molecule.sGroupForest.getSGroupsBFS();
sgorder.forEach(function (id) {
sgmapback[cnt] = id;
sgmap[id] = cnt++;
}, this);
for (var q = 1; q < cnt; ++q) { // each group on its own
var id = sgmapback[q];
var sgroup = this.molecule.sgroups.get(id);
this.write('M STY');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePadded(sgroup.type, 3);
this.writeCR();
// TODO: write subtype, M SST
this.write('M SLB');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeCR();
var parentid = this.molecule.sGroupForest.parent.get(id);
if (parentid >= 0) {
this.write('M SPL');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(sgmap[parentid], 3);
this.writeCR();
}
// connectivity
if (sgroup.type == 'SRU' && sgroup.data.connectivity) {
var connectivity = '';
connectivity += ' ';
connectivity += q.toString().padStart(3);
connectivity += ' ';
connectivity += (sgroup.data.connectivity || '').padEnd(3);
this.write('M SCN');
this.writePaddedNumber(1, 3);
this.write(connectivity.toUpperCase());
this.writeCR();
}
if (sgroup.type == 'SRU') {
this.write('M SMT ');
this.writePaddedNumber(q, 3);
this.writeWhiteSpace();
this.write(sgroup.data.subscript || 'n');
this.writeCR();
}
this.writeCR(common.saveToMolfile[sgroup.type](sgroup, this.molecule, sgmap, this.mapping, this.bondMapping));
}
// TODO: write M APO
// TODO: write M AAL
// TODO: write M RGP
// TODO: write M LOG
this.writeCR('M END');
};
module.exports = Molfile;

View File

@ -0,0 +1,301 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Struct = require('./../struct/index');
var utils = require('./utils');
function readKeyValuePairs(str, /* bool */ valueString) {
/* reader */
var ret = {};
var partition = utils.partitionLineFixed(str, 3, true);
var count = utils.parseDecimalInt(partition[0]);
for (var i = 0; i < count; ++i) {
/* eslint-disable no-mixed-operators*/
ret[utils.parseDecimalInt(partition[2 * i + 1]) - 1] =
valueString ? partition[2 * i + 2].trim() :
utils.parseDecimalInt(partition[2 * i + 2]);
/* eslint-enable no-mixed-operators*/
}
return ret;
}
function readKeyMultiValuePairs(str, /* bool */ valueString) {
/* reader */
var ret = [];
var partition = utils.partitionLineFixed(str, 3, true);
var count = utils.parseDecimalInt(partition[0]);
for (var i = 0; i < count; ++i) {
ret.push([
/* eslint-disable no-mixed-operators*/
utils.parseDecimalInt(partition[2 * i + 1]) - 1,
valueString ? partition[2 * i + 2].trim() : utils.parseDecimalInt(partition[2 * i + 2])
/* eslint-enable no-mixed-operators*/
]);
}
return ret;
}
function postLoadMul(sgroup, mol, atomMap) { // eslint-disable-line max-statements
sgroup.data.mul = sgroup.data.subscript - 0;
var atomReductionMap = {};
sgroup.atoms = Struct.SGroup.filterAtoms(sgroup.atoms, atomMap);
sgroup.patoms = Struct.SGroup.filterAtoms(sgroup.patoms, atomMap);
// mark repetitions for removal
for (var k = 1; k < sgroup.data.mul; ++k) {
for (var m = 0; m < sgroup.patoms.length; ++m) {
var raid = sgroup.atoms[k * sgroup.patoms.length + m]; // eslint-disable-line no-mixed-operators
if (raid < 0)
continue; // eslint-disable-line no-continue
if (sgroup.patoms[m] < 0)
throw new Error('parent atom missing');
atomReductionMap[raid] = sgroup.patoms[m]; // "merge" atom in parent
}
}
sgroup.patoms = Struct.SGroup.removeNegative(sgroup.patoms);
var patomsMap = identityMap(sgroup.patoms);
var bondsToRemove = [];
mol.bonds.each(function (bid, bond) {
var beginIn = bond.begin in atomReductionMap;
var endIn = bond.end in atomReductionMap;
// if both adjacent atoms of a bond are to be merged, remove it
/* eslint-disable no-mixed-operators*/
if (beginIn && endIn ||
beginIn && bond.end in patomsMap ||
endIn && bond.begin in patomsMap)
bondsToRemove.push(bid);
/* eslint-enable no-mixed-operators*/
// if just one atom is merged, modify the bond accordingly
else if (beginIn)
bond.begin = atomReductionMap[bond.begin];
else if (endIn)
bond.end = atomReductionMap[bond.end];
}, sgroup);
// apply removal lists
for (var b = 0; b < bondsToRemove.length; ++b)
mol.bonds.remove(bondsToRemove[b]);
for (var a in atomReductionMap) {
mol.atoms.remove(a);
atomMap[a] = -1;
}
sgroup.atoms = sgroup.patoms;
sgroup.patoms = null;
}
function postLoadSru(sgroup) {
sgroup.data.connectivity = (sgroup.data.connectivity || 'EU').trim().toLowerCase();
}
function postLoadSup(sgroup) {
sgroup.data.name = (sgroup.data.subscript || '').trim();
sgroup.data.subscript = '';
}
function postLoadGen(sgroup, mol, atomMap) { // eslint-disable-line no-unused-vars
}
function postLoadDat(sgroup, mol) {
if (!sgroup.data.absolute)
sgroup.pp = sgroup.pp.add(Struct.SGroup.getMassCentre(mol, sgroup.atoms));
}
function loadSGroup(mol, sg, atomMap) {
var postLoadMap = {
MUL: postLoadMul,
SRU: postLoadSru,
SUP: postLoadSup,
DAT: postLoadDat,
GEN: postLoadGen
};
// add the group to the molecule
sg.id = mol.sgroups.add(sg);
// apply type-specific post-processing
postLoadMap[sg.type](sg, mol, atomMap);
// mark atoms in the group as belonging to it
for (var s = 0; s < sg.atoms.length; ++s) {
if (mol.atoms.has(sg.atoms[s]))
Set.add(mol.atoms.get(sg.atoms[s]).sgs, sg.id);
}
if (sg.type == 'DAT')
mol.sGroupForest.insert(sg.id, -1, []);
else
mol.sGroupForest.insert(sg.id);
return sg.id;
}
function initSGroup(sGroups, propData) {
/* reader */
var kv = readKeyValuePairs(propData, true);
for (var key in kv) {
var type = kv[key];
if (!(type in Struct.SGroup.TYPES))
throw new Error('Unsupported S-group type');
var sg = new Struct.SGroup(type);
sg.number = key;
sGroups[key] = sg;
}
}
function applySGroupProp(sGroups, propName, propData, numeric, core) { // eslint-disable-line max-params
var kv = readKeyValuePairs(propData, !(numeric));
for (var key in kv)
// "core" properties are stored directly in an sgroup, not in sgroup.data
(core ? sGroups[key] : sGroups[key].data)[propName] = kv[key];
}
function applySGroupArrayProp(sGroups, propName, propData, shift) {
/* reader */
var sid = utils.parseDecimalInt(propData.slice(1, 4)) - 1;
var num = utils.parseDecimalInt(propData.slice(4, 8));
var part = toIntArray(utils.partitionLineFixed(propData.slice(8), 3, true));
if (part.length != num)
throw new Error('File format invalid');
if (shift) {
part = part.map(function (v) {
return v + shift;
});
}
sGroups[sid][propName] = sGroups[sid][propName].concat(part);
}
function applyDataSGroupName(sg, name) {
/* reader */
sg.data.fieldName = name;
}
function applyDataSGroupQuery(sg, query) {
/* reader */
sg.data.query = query;
}
function applyDataSGroupQueryOp(sg, queryOp) {
/* reader */
sg.data.queryOp = queryOp;
}
function applyDataSGroupDesc(sGroups, propData) {
/* reader */
var split = utils.partitionLine(propData, [4, 31, 2, 20, 2, 3], false);
var id = utils.parseDecimalInt(split[0]) - 1;
var fieldName = split[1].trim();
var fieldType = split[2].trim();
var units = split[3].trim();
var query = split[4].trim();
var queryOp = split[5].trim();
var sGroup = sGroups[id];
sGroup.data.fieldType = fieldType;
sGroup.data.fieldName = fieldName;
sGroup.data.units = units;
sGroup.data.query = query;
sGroup.data.queryOp = queryOp;
}
function applyDataSGroupInfo(sg, propData) { // eslint-disable-line max-statements
/* reader */
var split = utils.partitionLine(propData, [10/* x.x*/, 10/* y.y*/, 4/* eee*/, 1/* f*/, 1/* g*/, 1/* h*/, 3/* i */, 3/* jjj*/, 3/* kkk*/, 3/* ll*/, 2/* m*/, 3/* n*/, 2/* oo*/], false);
var x = parseFloat(split[0]);
var y = parseFloat(split[1]);
var attached = split[3].trim() == 'A';
var absolute = split[4].trim() == 'A';
var showUnits = split[5].trim() == 'U';
var nCharsToDisplay = split[7].trim();
nCharsToDisplay = nCharsToDisplay == 'ALL' ? -1 : utils.parseDecimalInt(nCharsToDisplay);
var tagChar = split[10].trim();
var daspPos = utils.parseDecimalInt(split[11].trim());
sg.pp = new Vec2(x, -y);
sg.data.attached = attached;
sg.data.absolute = absolute;
sg.data.showUnits = showUnits;
sg.data.nCharsToDisplay = nCharsToDisplay;
sg.data.tagChar = tagChar;
sg.data.daspPos = daspPos;
}
function applyDataSGroupInfoLine(sGroups, propData) {
/* reader */
var id = utils.parseDecimalInt(propData.substr(0, 4)) - 1;
var sg = sGroups[id];
applyDataSGroupInfo(sg, propData.substr(5));
}
function applyDataSGroupData(sg, data, finalize) {
/* reader */
sg.data.fieldValue = (sg.data.fieldValue || '') + data;
if (finalize) {
sg.data.fieldValue = trimRight(sg.data.fieldValue);
if (sg.data.fieldValue.startsWith('"') && sg.data.fieldValue.endsWith('"'))
sg.data.fieldValue = sg.data.fieldValue.substr(1, sg.data.fieldValue.length - 2);
}
}
function applyDataSGroupDataLine(sGroups, propData, finalize) {
/* reader */
var id = utils.parseDecimalInt(propData.substr(0, 5)) - 1;
var data = propData.substr(5);
var sg = sGroups[id];
applyDataSGroupData(sg, data, finalize);
}
// Utilities functions
function toIntArray(strArray) {
/* reader */
var ret = [];
for (var j = 0; j < strArray.length; ++j)
ret[j] = utils.parseDecimalInt(strArray[j]);
return ret;
}
function trimRight(str) {
return str.replace(/\s+$/, '');
}
function identityMap(array) {
var map = {};
for (var i = 0; i < array.length; ++i)
map[array[i]] = array[i];
return map;
}
module.exports = {
readKeyValuePairs: readKeyValuePairs,
readKeyMultiValuePairs: readKeyMultiValuePairs,
loadSGroup: loadSGroup,
initSGroup: initSGroup,
applySGroupProp: applySGroupProp,
applySGroupArrayProp: applySGroupArrayProp,
applyDataSGroupName: applyDataSGroupName,
applyDataSGroupQuery: applyDataSGroupQuery,
applyDataSGroupQueryOp: applyDataSGroupQueryOp,
applyDataSGroupDesc: applyDataSGroupDesc,
applyDataSGroupInfo: applyDataSGroupInfo,
applyDataSGroupData: applyDataSGroupData,
applyDataSGroupInfoLine: applyDataSGroupInfoLine,
applyDataSGroupDataLine: applyDataSGroupDataLine
};

View File

@ -0,0 +1,274 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Struct = require('./../struct/index');
function paddedNum(number, width, precision) {
var numStr = number.toFixed(precision || 0).replace(',', '.'); // Really need to replace?
if (numStr.length > width)
throw new Error('number does not fit');
return numStr.padStart(width);
}
function parseDecimalInt(str) {
/* reader */
var val = parseInt(str, 10);
return isNaN(val) ? 0 : val;
}
function partitionLine(/* string*/ str, /* array of int*/ parts, /* bool*/ withspace) {
/* reader */
var res = [];
for (var i = 0, shift = 0; i < parts.length; ++i) {
res.push(str.slice(shift, shift + parts[i]));
if (withspace)
shift++;
shift += parts[i];
}
return res;
}
function partitionLineFixed(/* string*/ str, /* int*/ itemLength, /* bool*/ withspace) {
/* reader */
var res = [];
for (var shift = 0; shift < str.length; shift += itemLength) {
res.push(str.slice(shift, shift + itemLength));
if (withspace)
shift++;
}
return res;
}
var fmtInfo = {
bondTypeMap: {
1: Struct.Bond.PATTERN.TYPE.SINGLE,
2: Struct.Bond.PATTERN.TYPE.DOUBLE,
3: Struct.Bond.PATTERN.TYPE.TRIPLE,
4: Struct.Bond.PATTERN.TYPE.AROMATIC,
5: Struct.Bond.PATTERN.TYPE.SINGLE_OR_DOUBLE,
6: Struct.Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC,
7: Struct.Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC,
8: Struct.Bond.PATTERN.TYPE.ANY
},
bondStereoMap: {
0: Struct.Bond.PATTERN.STEREO.NONE,
1: Struct.Bond.PATTERN.STEREO.UP,
4: Struct.Bond.PATTERN.STEREO.EITHER,
6: Struct.Bond.PATTERN.STEREO.DOWN,
3: Struct.Bond.PATTERN.STEREO.CIS_TRANS
},
v30bondStereoMap: {
0: Struct.Bond.PATTERN.STEREO.NONE,
1: Struct.Bond.PATTERN.STEREO.UP,
2: Struct.Bond.PATTERN.STEREO.EITHER,
3: Struct.Bond.PATTERN.STEREO.DOWN
},
bondTopologyMap: {
0: Struct.Bond.PATTERN.TOPOLOGY.EITHER,
1: Struct.Bond.PATTERN.TOPOLOGY.RING,
2: Struct.Bond.PATTERN.TOPOLOGY.CHAIN
},
countsLinePartition: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6],
atomLinePartition: [10, 10, 10, 1, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
bondLinePartition: [3, 3, 3, 3, 3, 3, 3],
atomListHeaderPartition: [3, 1, 1, 4, 1, 1],
atomListHeaderLength: 11, // = atomListHeaderPartition.reduce(function(a,b) { return a + b; }, 0)
atomListHeaderItemLength: 4,
chargeMap: [0, +3, +2, +1, 0, -1, -2, -3],
valenceMap: [undefined, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0],
implicitHydrogenMap: [undefined, 0, 1, 2, 3, 4],
v30atomPropMap: {
CHG: 'charge',
RAD: 'radical',
MASS: 'isotope',
VAL: 'explicitValence',
HCOUNT: 'hCount',
INVRET: 'invRet',
SUBST: 'substitutionCount',
UNSAT: 'unsaturatedAtom',
RBCNT: 'ringBondCount'
},
rxnItemsPartition: [3, 3, 3]
};
var FRAGMENT = {
NONE: 0,
REACTANT: 1,
PRODUCT: 2,
AGENT: 3
};
var SHOULD_REACTION_FRAGMENT_RELAYOUT = true;
var SHOULD_RESCALE_MOLECULES = true;
function rxnMerge(mols, nReactants, nProducts) /* Struct */ { // eslint-disable-line max-statements
/* reader */
var ret = new Struct();
var bbReact = [],
bbAgent = [],
bbProd = [];
var molReact = [],
molAgent = [],
molProd = [];
var j;
var bondLengthData = { cnt: 0, totalLength: 0 };
for (j = 0; j < mols.length; ++j) {
var mol = mols[j];
var bondLengthDataMol = mol.getBondLengthData();
bondLengthData.cnt += bondLengthDataMol.cnt;
bondLengthData.totalLength += bondLengthDataMol.totalLength;
}
if (SHOULD_RESCALE_MOLECULES) {
var avgBondLength = 1 / (bondLengthData.cnt == 0 ? 1 : bondLengthData.totalLength / bondLengthData.cnt);
for (j = 0; j < mols.length; ++j) {
mol = mols[j];
mol.scale(avgBondLength);
}
}
for (j = 0; j < mols.length; ++j) {
mol = mols[j];
var bb = mol.getCoordBoundingBoxObj();
if (!bb)
continue; // eslint-disable-line no-continue
var fragmentType = (j < nReactants ? FRAGMENT.REACTANT : // eslint-disable-line no-nested-ternary
(j < nReactants + nProducts ? FRAGMENT.PRODUCT :
FRAGMENT.AGENT));
if (fragmentType == FRAGMENT.REACTANT) {
bbReact.push(bb);
molReact.push(mol);
} else if (fragmentType == FRAGMENT.AGENT) {
bbAgent.push(bb);
molAgent.push(mol);
} else if (fragmentType == FRAGMENT.PRODUCT) {
bbProd.push(bb);
molProd.push(mol);
}
mol.atoms.each(function (aid, atom) {
atom.rxnFragmentType = fragmentType;
});
}
function shiftMol(ret, mol, bb, xorig, over) { // eslint-disable-line max-params
var d = new Vec2(xorig - bb.min.x, over ? 1 - bb.min.y : -(bb.min.y + bb.max.y) / 2);
mol.atoms.each(function (aid, atom) {
atom.pp.add_(d); // eslint-disable-line no-underscore-dangle
});
mol.sgroups.each(function (id, item) {
if (item.pp)
item.pp.add_(d); // eslint-disable-line no-underscore-dangle
});
bb.min.add_(d); // eslint-disable-line no-underscore-dangle
bb.max.add_(d); // eslint-disable-line no-underscore-dangle
mol.mergeInto(ret);
return bb.max.x - bb.min.x;
}
if (SHOULD_REACTION_FRAGMENT_RELAYOUT) {
// reaction fragment layout
var xorig = 0;
for (j = 0; j < molReact.length; ++j)
xorig += shiftMol(ret, molReact[j], bbReact[j], xorig, false) + 2.0;
xorig += 2.0;
for (j = 0; j < molAgent.length; ++j)
xorig += shiftMol(ret, molAgent[j], bbAgent[j], xorig, true) + 2.0;
xorig += 2.0;
for (j = 0; j < molProd.length; ++j)
xorig += shiftMol(ret, molProd[j], bbProd[j], xorig, false) + 2.0;
} else {
for (j = 0; j < molReact.length; ++j)
molReact[j].mergeInto(ret);
for (j = 0; j < molAgent.length; ++j)
molAgent[j].mergeInto(ret);
for (j = 0; j < molProd.length; ++j)
molProd[j].mergeInto(ret);
}
var bb1;
var bb2;
var x;
var y;
var bbReactAll = null;
var bbProdAll = null;
for (j = 0; j < bbReact.length - 1; ++j) {
bb1 = bbReact[j];
bb2 = bbReact[j + 1];
x = (bb1.max.x + bb2.min.x) / 2;
y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;
ret.rxnPluses.add(new Struct.RxnPlus({ pp: new Vec2(x, y) }));
}
for (j = 0; j < bbReact.length; ++j) {
if (j == 0) {
bbReactAll = {};
bbReactAll.max = new Vec2(bbReact[j].max);
bbReactAll.min = new Vec2(bbReact[j].min);
} else {
bbReactAll.max = Vec2.max(bbReactAll.max, bbReact[j].max);
bbReactAll.min = Vec2.min(bbReactAll.min, bbReact[j].min);
}
}
for (j = 0; j < bbProd.length - 1; ++j) {
bb1 = bbProd[j];
bb2 = bbProd[j + 1];
x = (bb1.max.x + bb2.min.x) / 2;
y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;
ret.rxnPluses.add(new Struct.RxnPlus({ pp: new Vec2(x, y) }));
}
for (j = 0; j < bbProd.length; ++j) {
if (j == 0) {
bbProdAll = {};
bbProdAll.max = new Vec2(bbProd[j].max);
bbProdAll.min = new Vec2(bbProd[j].min);
} else {
bbProdAll.max = Vec2.max(bbProdAll.max, bbProd[j].max);
bbProdAll.min = Vec2.min(bbProdAll.min, bbProd[j].min);
}
}
bb1 = bbReactAll;
bb2 = bbProdAll;
if (!bb1 && !bb2) {
ret.rxnArrows.add(new Struct.RxnArrow({ pp: new Vec2(0, 0) }));
} else {
var v1 = bb1 ? new Vec2(bb1.max.x, (bb1.max.y + bb1.min.y) / 2) : null;
var v2 = bb2 ? new Vec2(bb2.min.x, (bb2.max.y + bb2.min.y) / 2) : null;
var defaultOffset = 3;
if (!v1)
v1 = new Vec2(v2.x - defaultOffset, v2.y);
if (!v2)
v2 = new Vec2(v1.x + defaultOffset, v1.y);
ret.rxnArrows.add(new Struct.RxnArrow({ pp: Vec2.lc2(v1, 0.5, v2, 0.5) }));
}
ret.isReaction = true;
return ret;
}
module.exports = {
fmtInfo: fmtInfo,
paddedNum: paddedNum,
parseDecimalInt: parseDecimalInt,
partitionLine: partitionLine,
partitionLineFixed: partitionLineFixed,
rxnMerge: rxnMerge
};

View File

@ -0,0 +1,426 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Map = require('../../util/map');
var element = require('./../element');
var Struct = require('./../struct/index');
var sGroup = require('./parseSGroup');
var utils = require('./utils');
var loadRGroupFragments = true; // TODO: set to load the fragments
function parseAtomLine(atomLine) {
/* reader */
var atomSplit = utils.partitionLine(atomLine, utils.fmtInfo.atomLinePartition);
var params =
{
// generic
pp: new Vec2(parseFloat(atomSplit[0]), -parseFloat(atomSplit[1]), parseFloat(atomSplit[2])),
label: atomSplit[4].trim(),
explicitValence: utils.fmtInfo.valenceMap[utils.parseDecimalInt(atomSplit[10])],
// obsolete
massDifference: utils.parseDecimalInt(atomSplit[5]),
charge: utils.fmtInfo.chargeMap[utils.parseDecimalInt(atomSplit[6])],
// query
hCount: utils.parseDecimalInt(utils.parseDecimalInt(atomSplit[8])),
stereoCare: utils.parseDecimalInt(atomSplit[9]) != 0,
// reaction
aam: utils.parseDecimalInt(atomSplit[14]),
invRet: utils.parseDecimalInt(atomSplit[15]),
// reaction query
exactChangeFlag: utils.parseDecimalInt(atomSplit[16]) != 0
};
return new Struct.Atom(params);
}
function parseBondLine(bondLine) {
/* reader */
var bondSplit = utils.partitionLine(bondLine, utils.fmtInfo.bondLinePartition);
var params =
{
begin: utils.parseDecimalInt(bondSplit[0]) - 1,
end: utils.parseDecimalInt(bondSplit[1]) - 1,
type: utils.fmtInfo.bondTypeMap[utils.parseDecimalInt(bondSplit[2])],
stereo: utils.fmtInfo.bondStereoMap[utils.parseDecimalInt(bondSplit[3])],
xxx: bondSplit[4],
topology: utils.fmtInfo.bondTopologyMap[utils.parseDecimalInt(bondSplit[5])],
reactingCenterStatus: utils.parseDecimalInt(bondSplit[6])
};
return new Struct.Bond(params);
}
function parseAtomListLine(/* string */atomListLine) {
/* reader */
var split = utils.partitionLine(atomListLine, utils.fmtInfo.atomListHeaderPartition);
var number = utils.parseDecimalInt(split[0]) - 1;
var notList = (split[2].trim() == 'T');
var count = utils.parseDecimalInt(split[4].trim());
var ids = atomListLine.slice(utils.fmtInfo.atomListHeaderLength);
var list = [];
var itemLength = utils.fmtInfo.atomListHeaderItemLength;
for (var i = 0; i < count; ++i)
list[i] = utils.parseDecimalInt(ids.slice(i * itemLength, ((i + 1) * itemLength) - 1));
return {
aid: number,
atomList: new Struct.AtomList({
notList: notList,
ids: list
})
};
}
function parsePropertyLines(ctab, ctabLines, shift, end, sGroups, rLogic) { // eslint-disable-line max-statements, max-params
/* reader */
var props = new Map();
while (shift < end) {
var line = ctabLines[shift];
if (line.charAt(0) == 'A') {
var propValue = ctabLines[++shift];
var isPseudo = /'.+'/.test(propValue);
if (isPseudo && !props.get('pseudo'))
props.set('pseudo', new Map());
if (!isPseudo && !props.get('alias'))
props.set('alias', new Map());
if (isPseudo) propValue = propValue.replace(/'/g, '');
props.get(isPseudo ? 'pseudo' : 'alias').set(utils.parseDecimalInt(line.slice(3, 6)) - 1, propValue);
} else if (line.charAt(0) == 'M') {
var type = line.slice(3, 6);
var propertyData = line.slice(6);
if (type == 'END') {
break;
} else if (type == 'CHG') {
if (!props.get('charge'))
props.set('charge', new Map());
props.get('charge').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'RAD') {
if (!props.get('radical'))
props.set('radical', new Map());
props.get('radical').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'ISO') {
if (!props.get('isotope'))
props.set('isotope', new Map());
props.get('isotope').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'RBC') {
if (!props.get('ringBondCount'))
props.set('ringBondCount', new Map());
props.get('ringBondCount').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'SUB') {
if (!props.get('substitutionCount'))
props.set('substitutionCount', new Map());
props.get('substitutionCount').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'UNS') {
if (!props.get('unsaturatedAtom'))
props.set('unsaturatedAtom', new Map());
props.get('unsaturatedAtom').update(sGroup.readKeyValuePairs(propertyData));
// else if (type == "LIN") // link atom
} else if (type == 'RGP') { // rgroup atom
if (!props.get('rglabel'))
props.set('rglabel', new Map());
var rglabels = props.get('rglabel');
var a2rs = sGroup.readKeyMultiValuePairs(propertyData);
for (var a2ri = 0; a2ri < a2rs.length; a2ri++) {
var a2r = a2rs[a2ri];
rglabels.set(a2r[0], (rglabels.get(a2r[0]) || 0) | (1 << (a2r[1] - 1)));
}
} else if (type == 'LOG') { // rgroup atom
propertyData = propertyData.slice(4);
var rgid = utils.parseDecimalInt(propertyData.slice(0, 3).trim());
var iii = utils.parseDecimalInt(propertyData.slice(4, 7).trim());
var hhh = utils.parseDecimalInt(propertyData.slice(8, 11).trim());
var ooo = propertyData.slice(12).trim();
var logic = {};
if (iii > 0)
logic.ifthen = iii;
logic.resth = hhh == 1;
logic.range = ooo;
rLogic[rgid] = logic;
} else if (type == 'APO') {
if (!props.get('attpnt'))
props.set('attpnt', new Map());
props.get('attpnt').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'ALS') { // atom list
if (!props.get('atomList'))
props.set('atomList', new Map());
var list = parsePropertyLineAtomList(
utils.partitionLine(propertyData, [1, 3, 3, 1, 1, 1]),
utils.partitionLineFixed(propertyData.slice(10), 4, false));
props.get('atomList').update(
list);
if (!props.get('label'))
props.set('label', new Map());
for (var aid in list) props.get('label').set(aid, 'L#');
} else if (type == 'STY') { // introduce s-group
sGroup.initSGroup(sGroups, propertyData);
} else if (type == 'SST') {
sGroup.applySGroupProp(sGroups, 'subtype', propertyData);
} else if (type == 'SLB') {
sGroup.applySGroupProp(sGroups, 'label', propertyData, true);
} else if (type == 'SPL') {
sGroup.applySGroupProp(sGroups, 'parent', propertyData, true, true);
} else if (type == 'SCN') {
sGroup.applySGroupProp(sGroups, 'connectivity', propertyData);
} else if (type == 'SAL') {
sGroup.applySGroupArrayProp(sGroups, 'atoms', propertyData, -1);
} else if (type == 'SBL') {
sGroup.applySGroupArrayProp(sGroups, 'bonds', propertyData, -1);
} else if (type == 'SPA') {
sGroup.applySGroupArrayProp(sGroups, 'patoms', propertyData, -1);
} else if (type == 'SMT') {
var sid = utils.parseDecimalInt(propertyData.slice(0, 4)) - 1;
sGroups[sid].data.subscript = propertyData.slice(4).trim();
} else if (type == 'SDT') {
sGroup.applyDataSGroupDesc(sGroups, propertyData);
} else if (type == 'SDD') {
sGroup.applyDataSGroupInfoLine(sGroups, propertyData);
} else if (type == 'SCD') {
sGroup.applyDataSGroupDataLine(sGroups, propertyData, false);
} else if (type == 'SED') {
sGroup.applyDataSGroupDataLine(sGroups, propertyData, true);
}
}
++shift;
}
return props;
}
function applyAtomProp(atoms /* Pool */, values /* Map */, propId /* string */) {
/* reader */
values.each(function (aid, propVal) {
atoms.get(aid)[propId] = propVal;
});
}
function parseCTabV2000(ctabLines, countsSplit) { // eslint-disable-line max-statements
/* reader */
var ctab = new Struct();
var i;
var atomCount = utils.parseDecimalInt(countsSplit[0]);
var bondCount = utils.parseDecimalInt(countsSplit[1]);
var atomListCount = utils.parseDecimalInt(countsSplit[2]);
ctab.isChiral = utils.parseDecimalInt(countsSplit[4]) != 0;
var stextLinesCount = utils.parseDecimalInt(countsSplit[5]);
var propertyLinesCount = utils.parseDecimalInt(countsSplit[10]);
var shift = 0;
var atomLines = ctabLines.slice(shift, shift + atomCount);
shift += atomCount;
var bondLines = ctabLines.slice(shift, shift + bondCount);
shift += bondCount;
var atomListLines = ctabLines.slice(shift, shift + atomListCount);
shift += atomListCount + stextLinesCount;
var atoms = atomLines.map(parseAtomLine);
for (i = 0; i < atoms.length; ++i)
ctab.atoms.add(atoms[i]);
var bonds = bondLines.map(parseBondLine);
for (i = 0; i < bonds.length; ++i)
ctab.bonds.add(bonds[i]);
var atomLists = atomListLines.map(parseAtomListLine);
atomLists.forEach(function (pair) {
ctab.atoms.get(pair.aid).atomList = pair.atomList;
ctab.atoms.get(pair.aid).label = 'L#';
});
var sGroups = {};
var rLogic = {};
var props = parsePropertyLines(ctab, ctabLines, shift,
Math.min(ctabLines.length, shift + propertyLinesCount), sGroups, rLogic);
props.each(function (propId, values) {
applyAtomProp(ctab.atoms, values, propId);
});
var atomMap = {};
var sid;
for (sid in sGroups) {
var sg = sGroups[sid];
if (sg.type === 'DAT' && sg.atoms.length === 0) {
var parent = sGroups[sid].parent;
if (parent >= 0) {
var psg = sGroups[parent - 1];
if (psg.type === 'GEN')
sg.atoms = [].slice.call(psg.atoms);
}
}
}
for (sid in sGroups)
sGroup.loadSGroup(ctab, sGroups[sid], atomMap);
var emptyGroups = [];
for (sid in sGroups) { // TODO: why do we need that?
Struct.SGroup.filter(ctab, sGroups[sid], atomMap);
if (sGroups[sid].atoms.length == 0 && !sGroups[sid].allAtoms)
emptyGroups.push(sid);
}
for (i = 0; i < emptyGroups.length; ++i) {
ctab.sGroupForest.remove(emptyGroups[i]);
ctab.sgroups.remove(emptyGroups[i]);
}
for (var rgid in rLogic)
ctab.rgroups.set(rgid, new Struct.RGroup(rLogic[rgid]));
return ctab;
}
function parseRg2000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
ctabLines = ctabLines.slice(7);
if (ctabLines[0].trim() != '$CTAB')
throw new Error('RGFile format invalid');
var i = 1;
while (ctabLines[i].charAt(0) != '$') i++;
if (ctabLines[i].trim() != '$END CTAB')
throw new Error('RGFile format invalid');
var coreLines = ctabLines.slice(1, i);
ctabLines = ctabLines.slice(i + 1);
var fragmentLines = {};
while (true) { // eslint-disable-line no-constant-condition
if (ctabLines.length == 0)
throw new Error('Unexpected end of file');
var line = ctabLines[0].trim();
if (line == '$END MOL') {
ctabLines = ctabLines.slice(1);
break;
}
if (line != '$RGP')
throw new Error('RGFile format invalid');
var rgid = ctabLines[1].trim() - 0;
fragmentLines[rgid] = [];
ctabLines = ctabLines.slice(2);
while (true) { // eslint-disable-line no-constant-condition
if (ctabLines.length == 0)
throw new Error('Unexpected end of file');
line = ctabLines[0].trim();
if (line == '$END RGP') {
ctabLines = ctabLines.slice(1);
break;
}
if (line != '$CTAB')
throw new Error('RGFile format invalid');
i = 1;
while (ctabLines[i].charAt(0) != '$') i++;
if (ctabLines[i].trim() != '$END CTAB')
throw new Error('RGFile format invalid');
fragmentLines[rgid].push(ctabLines.slice(1, i));
ctabLines = ctabLines.slice(i + 1);
}
}
var core = parseCTab(coreLines);
var frag = {};
if (loadRGroupFragments) {
for (var id in fragmentLines) {
frag[id] = [];
for (var j = 0; j < fragmentLines[id].length; ++j)
frag[id].push(parseCTab(fragmentLines[id][j]));
}
}
return rgMerge(core, frag);
}
function parseRxn2000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
ctabLines = ctabLines.slice(4);
var countsSplit = utils.partitionLine(ctabLines[0], utils.fmtInfo.rxnItemsPartition);
var nReactants = countsSplit[0] - 0,
nProducts = countsSplit[1] - 0,
nAgents = countsSplit[2] - 0;
ctabLines = ctabLines.slice(1); // consume counts line
var mols = [];
while (ctabLines.length > 0 && ctabLines[0].substr(0, 4) == '$MOL') {
ctabLines = ctabLines.slice(1);
var n = 0;
while (n < ctabLines.length && ctabLines[n].substr(0, 4) != '$MOL') n++;
var lines = ctabLines.slice(0, n);
var struct;
if (lines[0].search('\\$MDL') == 0) {
struct = parseRg2000(lines);
} else {
struct = parseCTab(lines.slice(3));
struct.name = lines[0].trim();
}
mols.push(struct);
ctabLines = ctabLines.slice(n);
}
return utils.rxnMerge(mols, nReactants, nProducts, nAgents);
}
function parseCTab(/* string */ ctabLines) /* Struct */ {
/* reader */
var countsSplit = utils.partitionLine(ctabLines[0], utils.fmtInfo.countsLinePartition);
ctabLines = ctabLines.slice(1);
return parseCTabV2000(ctabLines, countsSplit);
}
function rgMerge(scaffold, rgroups) /* Struct */ {
/* reader */
var ret = new Struct();
scaffold.mergeInto(ret, null, null, false, true);
for (var rgid in rgroups) {
for (var j = 0; j < rgroups[rgid].length; ++j) {
var ctab = rgroups[rgid][j];
ctab.rgroups.set(rgid, new Struct.RGroup());
var frag = {};
var frid = ctab.frags.add(frag);
ctab.rgroups.get(rgid).frags.add(frid);
ctab.atoms.each(function (aid, atom) {
atom.fragment = frid;
});
ctab.mergeInto(ret);
}
}
return ret;
}
function labelsListToIds(labels) {
/* reader */
var ids = [];
for (var i = 0; i < labels.length; ++i)
ids.push(element.map[labels[i].trim()]);
return ids;
}
function parsePropertyLineAtomList(hdr, lst) {
/* reader */
var aid = utils.parseDecimalInt(hdr[1]) - 1;
var count = utils.parseDecimalInt(hdr[2]);
var notList = hdr[4].trim() == 'T';
var ids = labelsListToIds(lst.slice(0, count));
var ret = {};
ret[aid] = new Struct.AtomList({
notList: notList,
ids: ids
});
return ret;
}
module.exports = {
parseCTabV2000: parseCTabV2000,
parseRg2000: parseRg2000,
parseRxn2000: parseRxn2000
};

View File

@ -0,0 +1,482 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var element = require('./../element');
var Struct = require('./../struct/index');
var sGroup = require('./parseSGroup');
var utils = require('./utils');
function parseAtomLineV3000(line) { // eslint-disable-line max-statements
/* reader */
var split, subsplit, key, value, i;
split = spaceparsplit(line);
var params = {
pp: new Vec2(parseFloat(split[2]), -parseFloat(split[3]), parseFloat(split[4])),
aam: split[5].trim()
};
var label = split[1].trim();
if (label.charAt(0) == '"' && label.charAt(label.length - 1) == '"')
label = label.substr(1, label.length - 2); // strip qutation marks
if (label.charAt(label.length - 1) == ']') { // assume atom list
label = label.substr(0, label.length - 1); // remove ']'
var atomListParams = {};
atomListParams.notList = false;
if (label.substr(0, 5) == 'NOT [') {
atomListParams.notList = true;
label = label.substr(5); // remove 'NOT ['
} else if (label.charAt(0) != '[') {
throw new Error('Error: atom list expected, found \'' + label + '\'');
} else {
label = label.substr(1); // remove '['
}
atomListParams.ids = labelsListToIds(label.split(','));
params['atomList'] = new Struct.AtomList(atomListParams);
params['label'] = 'L#';
} else {
params['label'] = label;
}
split.splice(0, 6);
for (i = 0; i < split.length; ++i) {
subsplit = splitonce(split[i], '=');
key = subsplit[0];
value = subsplit[1];
if (key in utils.fmtInfo.v30atomPropMap) {
var ival = utils.parseDecimalInt(value);
if (key == 'VAL') {
if (ival == 0)
continue; // eslint-disable-line no-continue
if (ival == -1)
ival = 0;
}
params[utils.fmtInfo.v30atomPropMap[key]] = ival;
} else if (key == 'RGROUPS') {
value = value.trim().substr(1, value.length - 2);
var rgrsplit = value.split(' ').slice(1);
params.rglabel = 0;
for (var j = 0; j < rgrsplit.length; ++j)
params.rglabel |= 1 << (rgrsplit[j] - 1);
} else if (key == 'ATTCHPT') {
params.attpnt = value.trim() - 0;
}
}
return new Struct.Atom(params);
}
function parseBondLineV3000(line) {
/* reader */
var split, subsplit, key, value, i;
split = spaceparsplit(line);
var params = {
begin: utils.parseDecimalInt(split[2]) - 1,
end: utils.parseDecimalInt(split[3]) - 1,
type: utils.fmtInfo.bondTypeMap[utils.parseDecimalInt(split[1])]
};
split.splice(0, 4);
for (i = 0; i < split.length; ++i) {
subsplit = splitonce(split[i], '=');
key = subsplit[0];
value = subsplit[1];
if (key == 'CFG') {
params.stereo = utils.fmtInfo.v30bondStereoMap[utils.parseDecimalInt(value)];
if (params.type == Struct.Bond.PATTERN.TYPE.DOUBLE && params.stereo == Struct.Bond.PATTERN.STEREO.EITHER)
params.stereo = Struct.Bond.PATTERN.STEREO.CIS_TRANS;
} else if (key == 'TOPO') {
params.topology = utils.fmtInfo.bondTopologyMap[utils.parseDecimalInt(value)];
} else if (key == 'RXCTR') {
params.reactingCenterStatus = utils.parseDecimalInt(value);
} else if (key == 'STBOX') {
params.stereoCare = utils.parseDecimalInt(value);
}
}
return new Struct.Bond(params);
}
function v3000parseCollection(ctab, ctabLines, shift) {
/* reader */
shift++;
while (ctabLines[shift].trim() != 'M V30 END COLLECTION')
shift++;
shift++;
return shift;
}
function v3000parseSGroup(ctab, ctabLines, sgroups, atomMap, shift) { // eslint-disable-line max-params, max-statements
/* reader */
var line = '';
shift++;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line.trim() == 'END SGROUP')
return shift;
while (line.charAt(line.length - 1) == '-')
line = (line.substr(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
var split = splitSGroupDef(line);
var type = split[1];
var sg = new Struct.SGroup(type);
sg.number = split[0] - 0;
sg.type = type;
sg.label = split[2] - 0;
sgroups[sg.number] = sg;
var props = {};
for (var i = 3; i < split.length; ++i) {
var subsplit = splitonce(split[i], '=');
if (subsplit.length != 2)
throw new Error('A record of form AAA=BBB or AAA=(...) expected, got \'' + split[i] + '\'');
var name = subsplit[0];
if (!(name in props))
props[name] = [];
props[name].push(subsplit[1]);
}
sg.atoms = parseBracedNumberList(props['ATOMS'][0], -1);
if (props['PATOMS'])
sg.patoms = parseBracedNumberList(props['PATOMS'][0], -1);
sg.bonds = props['BONDS'] ? parseBracedNumberList(props['BONDS'][0], -1) : [];
var brkxyzStrs = props['BRKXYZ'];
sg.brkxyz = [];
if (brkxyzStrs) {
for (var j = 0; j < brkxyzStrs.length; ++j)
sg.brkxyz.push(parseBracedNumberList(brkxyzStrs[j]));
}
if (props['MULT'])
sg.data.subscript = props['MULT'][0] - 0;
if (props['LABEL'])
sg.data.subscript = props['LABEL'][0].trim();
if (props['CONNECT'])
sg.data.connectivity = props['CONNECT'][0].toLowerCase();
if (props['FIELDDISP'])
sGroup.applyDataSGroupInfo(sg, stripQuotes(props['FIELDDISP'][0]));
if (props['FIELDDATA'])
sGroup.applyDataSGroupData(sg, props['FIELDDATA'][0], true);
if (props['FIELDNAME'])
sGroup.applyDataSGroupName(sg, props['FIELDNAME'][0]);
if (props['QUERYTYPE'])
sGroup.applyDataSGroupQuery(sg, props['QUERYTYPE'][0]);
if (props['QUERYOP'])
sGroup.applyDataSGroupQueryOp(sg, props['QUERYOP'][0]);
sGroup.loadSGroup(ctab, sg, atomMap);
}
throw new Error('S-group declaration incomplete.');
}
function parseCTabV3000(ctabLines, norgroups) { // eslint-disable-line max-statements
/* reader */
var ctab = new Struct();
var shift = 0;
if (ctabLines[shift++].trim() != 'M V30 BEGIN CTAB')
throw Error('CTAB V3000 invalid');
if (ctabLines[shift].slice(0, 13) != 'M V30 COUNTS')
throw Error('CTAB V3000 invalid');
var vals = ctabLines[shift].slice(14).split(' ');
ctab.isChiral = (utils.parseDecimalInt(vals[4]) == 1);
shift++;
if (ctabLines[shift].trim() == 'M V30 BEGIN ATOM') {
shift++;
var line;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line == 'END ATOM')
break;
while (line.charAt(line.length - 1) == '-')
line = (line.substring(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
ctab.atoms.add(parseAtomLineV3000(line));
}
if (ctabLines[shift].trim() == 'M V30 BEGIN BOND') {
shift++;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line == 'END BOND')
break;
while (line.charAt(line.length - 1) == '-')
line = (line.substring(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
ctab.bonds.add(parseBondLineV3000(line));
}
}
// TODO: let sections follow in arbitrary order
var sgroups = {};
var atomMap = {};
while (ctabLines[shift].trim() != 'M V30 END CTAB') {
if (ctabLines[shift].trim() == 'M V30 BEGIN COLLECTION')
// TODO: read collection information
shift = v3000parseCollection(ctab, ctabLines, shift);
else if (ctabLines[shift].trim() == 'M V30 BEGIN SGROUP')
shift = v3000parseSGroup(ctab, ctabLines, sgroups, atomMap, shift);
else
throw Error('CTAB V3000 invalid');
}
}
if (ctabLines[shift++].trim() != 'M V30 END CTAB')
throw Error('CTAB V3000 invalid');
if (!norgroups)
readRGroups3000(ctab, ctabLines.slice(shift));
return ctab;
}
function readRGroups3000(ctab, /* string */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
var rfrags = {};
var rLogic = {};
var shift = 0;
while (shift < ctabLines.length && ctabLines[shift].search('M V30 BEGIN RGROUP') == 0) {
var id = ctabLines[shift++].split(' ').pop();
rfrags[id] = [];
rLogic[id] = {};
while (true) { // eslint-disable-line no-constant-condition
var line = ctabLines[shift].trim();
if (line.search('M V30 RLOGIC') == 0) {
line = line.slice(13);
var rlsplit = line.trim().split(/\s+/g);
var iii = utils.parseDecimalInt(rlsplit[0]);
var hhh = utils.parseDecimalInt(rlsplit[1]);
var ooo = rlsplit.slice(2).join(' ');
var logic = {};
if (iii > 0)
logic.ifthen = iii;
logic.resth = hhh == 1;
logic.range = ooo;
rLogic[id] = logic;
shift++;
continue; // eslint-disable-line no-continue
}
if (line != 'M V30 BEGIN CTAB')
throw Error('CTAB V3000 invalid');
for (var i = 0; i < ctabLines.length; ++i) {
if (ctabLines[shift + i].trim() == 'M V30 END CTAB')
break;
}
var lines = ctabLines.slice(shift, shift + i + 1);
var rfrag = parseCTabV3000(lines, true);
rfrags[id].push(rfrag);
shift = shift + i + 1;
if (ctabLines[shift].trim() == 'M V30 END RGROUP') {
shift++;
break;
}
}
}
for (var rgid in rfrags) {
for (var j = 0; j < rfrags[rgid].length; ++j) {
var rg = rfrags[rgid][j];
rg.rgroups.set(rgid, new Struct.RGroup(rLogic[rgid]));
var frid = rg.frags.add({});
rg.rgroups.get(rgid).frags.add(frid);
rg.atoms.each(function (aid, atom) {
atom.fragment = frid;
});
rg.mergeInto(ctab);
}
}
}
function parseRxn3000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
ctabLines = ctabLines.slice(4);
var countsSplit = ctabLines[0].split(/\s+/g).slice(3);
var nReactants = countsSplit[0] - 0,
nProducts = countsSplit[1] - 0,
nAgents = countsSplit.length > 2 ? countsSplit[2] - 0 : 0;
function findCtabEnd(i) {
for (var j = i; j < ctabLines.length; ++j) {
if (ctabLines[j].trim() == 'M V30 END CTAB')
return j;
}
console.error('CTab format invalid');
}
function findRGroupEnd(i) {
for (var j = i; j < ctabLines.length; ++j) {
if (ctabLines[j].trim() == 'M V30 END RGROUP')
return j;
}
console.error('CTab format invalid');
}
var molLinesReactants = [];
var molLinesProducts = [];
var current = null;
var rGroups = [];
for (var i = 0; i < ctabLines.length; ++i) {
var line = ctabLines[i].trim();
var j;
if (line.startsWith('M V30 COUNTS')) {
// do nothing
} else if (line == 'M END') {
break; // stop reading
} else if (line == 'M V30 BEGIN PRODUCT') {
console.assert(current == null, 'CTab format invalid');
current = molLinesProducts;
} else if (line == 'M V30 END PRODUCT') {
console.assert(current === molLinesProducts, 'CTab format invalid');
current = null;
} else if (line == 'M V30 BEGIN REACTANT') {
console.assert(current == null, 'CTab format invalid');
current = molLinesReactants;
} else if (line == 'M V30 END REACTANT') {
console.assert(current === molLinesReactants, 'CTab format invalid');
current = null;
} else if (line.startsWith('M V30 BEGIN RGROUP')) {
console.assert(current == null, 'CTab format invalid');
j = findRGroupEnd(i);
rGroups.push(ctabLines.slice(i, j + 1));
i = j;
} else if (line == 'M V30 BEGIN CTAB') {
j = findCtabEnd(i);
current.push(ctabLines.slice(i, j + 1));
i = j;
} else {
throw new Error('line unrecognized: ' + line);
}
}
var mols = [];
var molLines = molLinesReactants.concat(molLinesProducts);
for (j = 0; j < molLines.length; ++j) {
var mol = parseCTabV3000(molLines[j], countsSplit);
mols.push(mol);
}
var ctab = utils.rxnMerge(mols, nReactants, nProducts, nAgents);
readRGroups3000(ctab, function (array) {
var res = [];
for (var k = 0; k < array.length; ++k)
res = res.concat(array[k]);
return res;
}(rGroups));
return ctab;
}
// split a line by spaces outside parentheses
function spaceparsplit(line) { // eslint-disable-line max-statements
/* reader */
var split = [];
var pc = 0;
var c;
var i;
var i0 = -1;
var quoted = false;
for (i = 0; i < line.length; ++i) {
c = line[i];
if (c == '(')
pc++;
else if (c == ')')
pc--;
if (c == '"')
quoted = !quoted;
if (!quoted && line[i] == ' ' && pc == 0) {
if (i > i0 + 1)
split.push(line.slice(i0 + 1, i));
i0 = i;
}
}
if (i > i0 + 1)
split.push(line.slice(i0 + 1, i));
return split;
}
// utils
function stripQuotes(str) {
if (str[0] === '"' && str[str.length - 1] === '"')
return str.substr(1, str.length - 2);
return str;
}
function splitonce(line, delim) {
/* reader */
var p = line.indexOf(delim);
return [line.slice(0, p), line.slice(p + 1)];
}
function splitSGroupDef(line) { // eslint-disable-line max-statements
/* reader */
var split = [];
var braceBalance = 0;
var quoted = false;
for (var i = 0; i < line.length; ++i) {
var c = line.charAt(i);
if (c == '"') {
quoted = !quoted;
} else if (!quoted) {
if (c == '(') {
braceBalance++;
} else if (c == ')') {
braceBalance--;
} else if (c == ' ' && braceBalance == 0) {
split.push(line.slice(0, i));
line = line.slice(i + 1).trim();
i = 0;
}
}
}
if (braceBalance != 0)
throw new Error('Brace balance broken. S-group properies invalid!');
if (line.length > 0)
split.push(line.trim());
return split;
}
function parseBracedNumberList(line, shift) {
/* reader */
if (!line)
return null;
var list = [];
line = line.trim();
line = line.substr(1, line.length - 2);
var split = line.split(' ');
shift = shift || 0;
for (var i = 1; i < split.length; ++i) {
var value = parseInt(split[i]);
if (!isNaN(value))
list.push(value + shift);
}
return list;
}
function stripV30(line) {
/* reader */
if (line.slice(0, 7) != 'M V30 ')
throw new Error('Prefix invalid');
return line.slice(7);
}
function labelsListToIds(labels) {
/* reader */
var ids = [];
for (var i = 0; i < labels.length; ++i)
ids.push(element.map[labels[i].trim()]);
return ids;
}
module.exports = {
parseCTabV3000: parseCTabV3000,
readRGroups3000: readRGroups3000,
parseRxn3000: parseRxn3000
};

View File

@ -0,0 +1,64 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var molfile = require('./molfile');
function parse(str, options) {
var regexp = /^[^]+?\$\$\$\$$/gm;
var m, chunk;
var result = [];
while ((m = regexp.exec(str)) !== null) {
chunk = m[0].replace(/\r/g, ''); // TODO: normalize newline?
chunk = chunk.trim();
var end = chunk.indexOf('M END');
if (end !== -1) {
var item = {};
var propChunks = chunk.substr(end + 7).trim().split(/^$\n?/m);
item.struct = molfile.parse(chunk.substring(0, end + 6), options);
item.props = propChunks.reduce(function (props, pc) {
var m = pc.match(/^> [ \d]*<(\S+)>/);
if (m) {
var field = m[1];
var value = pc.split('\n')[1].trim();
props[field] = value;
}
return props;
}, {});
result.push(item);
}
}
return result;
}
function stringify(items, options) {
return items.reduce(function (res, item) {
res += molfile.stringify(item.struct, options);
for (var prop in item.props) {
res += "> <" + prop + ">\n";
res += item.props[prop] + "\n\n";
}
return res + '\$\$\$\$';
}, '');
}
module.exports = {
stringify: stringify,
parse: parse
};

View File

@ -0,0 +1,212 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Map = require('../../util/map');
var Vec2 = require('../../util/vec2');
var Struct = require('../struct');
function CisTrans(mol, neighborsFunc, context) {
this.molecule = mol;
this.bonds = new Map();
this.getNeighbors = neighborsFunc;
this.context = context;
}
CisTrans.PARITY = {
NONE: 0,
CIS: 1,
TRANS: 2
};
CisTrans.prototype.each = function (func, context) {
this.bonds.each(func, context);
};
CisTrans.prototype.getParity = function (idx) {
return this.bonds.get(idx).parity;
};
CisTrans.prototype.getSubstituents = function (idx) {
return this.bonds.get(idx).substituents;
};
CisTrans.prototype.sameside = function (beg, end, neiBeg, neiEnd) {
var diff = Vec2.diff(beg, end);
var norm = new Vec2(-diff.y, diff.x);
if (!norm.normalize())
return 0;
var normBeg = Vec2.diff(neiBeg, beg);
var normEnd = Vec2.diff(neiEnd, end);
if (!normBeg.normalize())
return 0;
if (!normEnd.normalize())
return 0;
var prodBeg = Vec2.dot(normBeg, norm);
var prodEnd = Vec2.dot(normEnd, norm);
if (Math.abs(prodBeg) < 0.001 || Math.abs(prodEnd) < 0.001)
return 0;
return (prodBeg * prodEnd > 0) ? 1 : -1;
};
CisTrans.prototype.samesides = function (iBeg, iEnd, iNeiBeg, iNeiEnd) {
return this.sameside(this.molecule.atoms.get(iBeg).pp, this.molecule.atoms.get(iEnd).pp,
this.molecule.atoms.get(iNeiBeg).pp, this.molecule.atoms.get(iNeiEnd).pp);
};
CisTrans.prototype.sortSubstituents = function (substituents) { // eslint-disable-line max-statements
var h0 = this.molecule.atoms.get(substituents[0]).pureHydrogen();
var h1 = substituents[1] < 0 || this.molecule.atoms.get(substituents[1]).pureHydrogen();
var h2 = this.molecule.atoms.get(substituents[2]).pureHydrogen();
var h3 = substituents[3] < 0 || this.molecule.atoms.get(substituents[3]).pureHydrogen();
if (h0 && h1)
return false;
if (h2 && h3)
return false;
if (h1) {
substituents[1] = -1;
} else if (h0) {
substituents[0] = substituents[1];
substituents[1] = -1;
} else if (substituents[0] > substituents[1]) {
swap(substituents, 0, 1);
}
if (h3) {
substituents[3] = -1;
} else if (h2) {
substituents[2] = substituents[3];
substituents[3] = -1;
} else if (substituents[2] > substituents[3]) {
swap(substituents, 2, 3);
}
return true;
};
CisTrans.prototype.isGeomStereoBond = function (bondIdx, substituents) { // eslint-disable-line max-statements
// it must be [C,N,Si]=[C,N,Si] bond
var bond = this.molecule.bonds.get(bondIdx);
if (bond.type != Struct.Bond.PATTERN.TYPE.DOUBLE)
return false;
var label1 = this.molecule.atoms.get(bond.begin).label;
var label2 = this.molecule.atoms.get(bond.end).label;
if (label1 != 'C' && label1 != 'N' && label1 != 'Si' && label1 != 'Ge')
return false;
if (label2 != 'C' && label2 != 'N' && label2 != 'Si' && label2 != 'Ge')
return false;
// the atoms should have 1 or 2 single bonds
// (apart from the double bond under consideration)
var neiBegin = this.getNeighbors.call(this.context, bond.begin);
var neiЕnd = this.getNeighbors.call(this.context, bond.end);
if (
neiBegin.length < 2 || neiBegin.length > 3 ||
neiЕnd.length < 2 || neiЕnd.length > 3
)
return false;
substituents[0] = -1;
substituents[1] = -1;
substituents[2] = -1;
substituents[3] = -1;
var i;
var nei;
for (i = 0; i < neiBegin.length; i++) {
nei = neiBegin[i];
if (nei.bid == bondIdx)
continue; // eslint-disable-line no-continue
if (this.molecule.bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE)
return false;
if (substituents[0] == -1)
substituents[0] = nei.aid;
else // (substituents[1] == -1)
substituents[1] = nei.aid;
}
for (i = 0; i < neiЕnd.length; i++) {
nei = neiЕnd[i];
if (nei.bid == bondIdx)
continue; // eslint-disable-line no-continue
if (this.molecule.bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE)
return false;
if (substituents[2] == -1)
substituents[2] = nei.aid;
else // (substituents[3] == -1)
substituents[3] = nei.aid;
}
if (substituents[1] != -1 && this.samesides(bond.begin, bond.end, substituents[0], substituents[1]) != -1)
return false;
if (substituents[3] != -1 && this.samesides(bond.begin, bond.end, substituents[2], substituents[3]) != -1)
return false;
return true;
};
CisTrans.prototype.build = function (excludeBonds) {
this.molecule.bonds.each(function (bid, bond) {
var ct = this.bonds.set(bid,
{
parity: 0,
substituents: []
});
if (Array.isArray(excludeBonds) && excludeBonds[bid])
return;
if (!this.isGeomStereoBond(bid, ct.substituents))
return;
if (!this.sortSubstituents(ct.substituents))
return;
var sign = this.samesides(bond.begin, bond.end, ct.substituents[0], ct.substituents[2]);
if (sign == 1)
ct.parity = CisTrans.PARITY.CIS;
else if (sign == -1)
ct.parity = CisTrans.PARITY.TRANS;
}, this);
};
function swap(arr, i1, i2) {
var tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
module.exports = CisTrans;

View File

@ -0,0 +1,177 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Set = require('../../util/set');
function Dfs(mol, atomData, components, nReactants) {
this.molecule = mol;
this.atom_data = atomData;
this.components = components;
this.nComponentsInReactants = -1;
this.nReactants = nReactants;
this.vertices = new Array(this.molecule.atoms.count()); // Minimum size
this.molecule.atoms.each(function (aid) {
this.vertices[aid] = new Dfs.VertexDesc();
}, this);
this.edges = new Array(this.molecule.bonds.count()); // Minimum size
this.molecule.bonds.each(function (bid) {
this.edges[bid] = new Dfs.EdgeDesc();
}, this);
this.v_seq = [];
}
Dfs.VertexDesc = function () {
this.dfs_state = 0; // 0 -- not on stack
// 1 -- on stack
// 2 -- removed from stack
this.parent_vertex = 0; // parent vertex in DFS tree
this.parent_edge = 0; // edge to parent vertex
this.branches = 0; // how many DFS branches go out from this vertex}
};
Dfs.EdgeDesc = function () {
this.opening_cycles = 0; // how many cycles are
// (i) starting with this edge
// and (ii) ending in this edge's first vertex
this.closing_cycle = 0; // 1 if this edge closes a cycle
};
Dfs.SeqElem = function (vIdx, parVertex, parEdge) {
this.idx = vIdx; // index of vertex in _graph
this.parent_vertex = parVertex; // parent vertex in DFS tree
this.parent_edge = parEdge; // edge to parent vertex
};
Dfs.prototype.walk = function () { // eslint-disable-line max-statements
var vStack = [];
var i, j;
var cid = 0;
var component = 0;
while (true) { // eslint-disable-line no-constant-condition
if (vStack.length < 1) {
var selected = -1;
var findFunc = function (aid) { // eslint-disable-line func-style
if (this.vertices[aid].dfs_state == 0) {
selected = aid;
return true;
}
return false;
};
while (cid < this.components.length && selected == -1) {
selected = Set.find(this.components[cid], findFunc, this);
if (selected === null) {
selected = -1;
cid++;
}
if (cid == this.nReactants)
this.nComponentsInReactants = component;
}
if (selected < -1)
this.molecule.atoms.find(findFunc, this);
if (selected == -1)
break;
this.vertices[selected].parent_vertex = -1;
this.vertices[selected].parent_edge = -1;
vStack.push(selected);
component++;
}
var vIdx = vStack.pop();
var parentVertex = this.vertices[vIdx].parent_vertex;
var seqElem = new Dfs.SeqElem(vIdx, parentVertex, this.vertices[vIdx].parent_edge);
this.v_seq.push(seqElem);
this.vertices[vIdx].dfs_state = 2;
var atomD = this.atom_data[vIdx];
for (i = 0; i < atomD.neighbours.length; i++) {
var neiIdx = atomD.neighbours[i].aid;
var edgeIdx = atomD.neighbours[i].bid;
if (neiIdx == parentVertex)
continue; // eslint-disable-line no-continue
if (this.vertices[neiIdx].dfs_state == 2) {
this.edges[edgeIdx].closing_cycle = 1;
j = vIdx;
while (j != -1 && this.vertices[j].parent_vertex != neiIdx)
j = this.vertices[j].parent_vertex;
if (j == -1)
throw new Error('cycle unwind error');
this.edges[this.vertices[j].parent_edge].opening_cycles++;
this.vertices[vIdx].branches++;
seqElem = new Dfs.SeqElem(neiIdx, vIdx, edgeIdx);
this.v_seq.push(seqElem);
} else {
if (this.vertices[neiIdx].dfs_state == 1) {
j = vStack.indexOf(neiIdx);
if (j == -1) // eslint-disable-line max-depth
throw new Error('internal: removing vertex from stack');
vStack.splice(j, 1);
var parent = this.vertices[neiIdx].parent_vertex;
if (parent >= 0) // eslint-disable-line max-depth
this.vertices[parent].branches--;
}
this.vertices[vIdx].branches++;
this.vertices[neiIdx].parent_vertex = vIdx;
this.vertices[neiIdx].parent_edge = edgeIdx;
this.vertices[neiIdx].dfs_state = 1;
vStack.push(neiIdx);
}
}
}
};
Dfs.prototype.edgeClosingCycle = function (eIdx) {
return this.edges[eIdx].closing_cycle != 0;
};
Dfs.prototype.numBranches = function (vIdx) {
return this.vertices[vIdx].branches;
};
Dfs.prototype.numOpeningCycles = function (eIdx) {
return this.edges[eIdx].opening_cycles;
};
Dfs.prototype.toString = function () {
var str = '';
this.v_seq.each(function (seqElem) {
str += seqElem.idx + ' -> ';
});
str += '*';
return str;
};
module.exports = Dfs;

View File

@ -0,0 +1,735 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Set = require('../../util/set');
var Struct = require('../struct');
var CisTrans = require('./cis_trans');
var Dfs = require('./dfs');
var Stereocenters = require('./stereocenters');
function Smiles() {
this.smiles = '';
this.writtenAtoms = [];
this.writtenComponents = 0;
this.ignore_errors = false;
}
Smiles._Atom = function (hСount) { // eslint-disable-line no-underscore-dangle
this.neighbours = []; // Array of integer pairs {a, b}
this.aromatic = false; // has aromatic bond
this.lowercase = false; // aromatic and has to be written lowercase
this.chirality = 0; // 0 means no chirality, 1 means CCW pyramid, 2 means CW pyramid
this.branch_cnt = 0; // runs from 0 to (branches - 1)
this.paren_written = false;
this.h_count = hСount;
this.parent = -1;
};
// NB: only loops of length up to 6 are included here
Smiles.prototype.isBondInRing = function (bid) {
console.assert(this.inLoop, 'Init this.inLoop prior to calling this method');
return this.inLoop[bid];
};
Smiles.prototype.saveMolecule = function (molecule, ignoreErrors) { // eslint-disable-line max-statements
var i, j, k;
if (!ignoreErrors) this.ignore_errors = ignoreErrors;
// [RB]: KETCHER-498 (Incorrect smile-string for multiple Sgroup)
// TODO the fix is temporary, still need to implement error handling/reporting
// BEGIN
molecule = molecule.clone();
molecule.initHalfBonds();
molecule.initNeighbors();
molecule.sortNeighbors();
molecule.setImplicitHydrogen();
molecule.sgroups.each(function (sgid, sg) {
if (sg.type == 'MUL') {
try {
Struct.SGroup.prepareMulForSaving(sg, molecule);
} catch (ex) {
throw { message: 'Bad s-group (' + ex.message + ')' };
}
}
// 'SMILES data format doesn\'t support s-groups'
}, this);
// END
this.atoms = new Array(molecule.atoms.count());
molecule.atoms.each(function (aid, atom) {
this.atoms[aid] = new Smiles._Atom(atom.implicitH); // eslint-disable-line no-underscore-dangle
}, this);
// From the SMILES specification:
// Please note that only atoms on the following list
// can be considered aromatic: C, N, O, P, S, As, Se, and * (wildcard).
var allowedLowercase = ['B', 'C', 'N', 'O', 'P', 'S', 'Se', 'As'];
// Detect atoms that have aromatic bonds and count neighbours
molecule.bonds.each(function (bid, bond) {
if (bond.type == Struct.Bond.PATTERN.TYPE.AROMATIC) {
this.atoms[bond.begin].aromatic = true;
if (allowedLowercase.indexOf(molecule.atoms.get(bond.begin).label) != -1)
this.atoms[bond.begin].lowercase = true;
this.atoms[bond.end].aromatic = true;
if (allowedLowercase.indexOf(molecule.atoms.get(bond.end).label) != -1)
this.atoms[bond.end].lowercase = true;
}
this.atoms[bond.begin].neighbours.push({ aid: bond.end, bid: bid });
this.atoms[bond.end].neighbours.push({ aid: bond.begin, bid: bid });
}, this);
this.inLoop = (function () {
molecule.prepareLoopStructure();
var bondsInLoops = Set.empty();
molecule.loops.each(function (lid, loop) {
if (loop.hbs.length <= 6) {
Set.mergeIn(bondsInLoops, Set.fromList(loop.hbs.map(function (hbid) {
return molecule.halfBonds.get(hbid).bid;
})));
}
});
var inLoop = {};
Set.each(bondsInLoops, function (bid) {
inLoop[bid] = 1;
}, this);
return inLoop;
})();
this.touchedCistransbonds = 0;
this.markCisTrans(molecule);
var components = molecule.getComponents();
var componentsAll = components.reactants.concat(components.products);
var walk = new Dfs(molecule, this.atoms, componentsAll, components.reactants.length);
walk.walk();
this.atoms.forEach(function (atom) {
atom.neighbours = [];
}, this);
// fill up neighbor lists for the stereocenters calculation
for (i = 0; i < walk.v_seq.length; i++) {
var seqEl = walk.v_seq[i];
var vIdx = seqEl.idx;
var eIdx = seqEl.parent_edge;
var vPrevIdx = seqEl.parent_vertex;
if (eIdx >= 0) {
var atom = this.atoms[vIdx];
var openingCycles = walk.numOpeningCycles(eIdx);
for (j = 0; j < openingCycles; j++)
this.atoms[vPrevIdx].neighbours.push({ aid: -1, bid: -1 });
if (walk.edgeClosingCycle(eIdx)) {
for (k = 0; k < atom.neighbours.length; k++) {
if (atom.neighbours[k].aid == -1) { // eslint-disable-line max-depth
atom.neighbours[k].aid = vPrevIdx;
atom.neighbours[k].bid = eIdx;
break;
}
}
if (k == atom.neighbours.length)
throw new Error('internal: can not put closing bond to its place');
} else {
atom.neighbours.push({ aid: vPrevIdx, bid: eIdx });
atom.parent = vPrevIdx;
}
this.atoms[vPrevIdx].neighbours.push({ aid: vIdx, bid: eIdx });
}
}
try {
// detect chiral configurations
var stereocenters = new Stereocenters(molecule, function (idx) {
return this.atoms[idx].neighbours;
}, this);
stereocenters.buildFromBonds(this.ignore_errors);
stereocenters.each(function (atomIdx, sc) { // eslint-disable-line max-statements
// if (sc.type < MoleculeStereocenters::ATOM_AND)
// continue;
var implicitHIdx = -1;
if (sc.pyramid[3] == -1)
implicitHIdx = 3;
/*
else for (j = 0; j < 4; j++)
if (ignored_vertices[pyramid[j]])
{
implicit_h_idx = j;
break;
}
*/
var pyramidMapping = [];
var counter = 0;
var atom = this.atoms[atomIdx];
if (atom.parent != -1) {
for (k = 0; k < 4; k++) {
if (sc.pyramid[k] == atom.parent) {
pyramidMapping[counter++] = k;
break;
}
}
}
if (implicitHIdx != -1)
pyramidMapping[counter++] = implicitHIdx;
for (j = 0; j != atom.neighbours.length; j++) {
if (atom.neighbours[j].aid == atom.parent)
continue; // eslint-disable-line no-continue
for (k = 0; k < 4; k++) {
if (atom.neighbours[j].aid == sc.pyramid[k]) {
if (counter >= 4)
throw new Error('internal: pyramid overflow');
pyramidMapping[counter++] = k;
break;
}
}
}
if (counter == 4) {
// move the 'from' atom to the end
counter = pyramidMapping[0];
pyramidMapping[0] = pyramidMapping[1];
pyramidMapping[1] = pyramidMapping[2];
pyramidMapping[2] = pyramidMapping[3];
pyramidMapping[3] = counter;
} else if (counter != 3) {
throw new Error('cannot calculate chirality');
}
if (Stereocenters.isPyramidMappingRigid(pyramidMapping))
this.atoms[atomIdx].chirality = 1;
else
this.atoms[atomIdx].chirality = 2;
}, this);
} catch (ex) {
alert('Warning: ' + ex.message);
}
// write the SMILES itself
// cycle_numbers[i] == -1 means that the number is available
// cycle_numbers[i] == n means that the number is used by vertex n
var cycleNumbers = [];
cycleNumbers.push(0); // never used
var firstComponent = true;
for (i = 0; i < walk.v_seq.length; i++) {
seqEl = walk.v_seq[i];
vIdx = seqEl.idx;
eIdx = seqEl.parent_edge;
vPrevIdx = seqEl.parent_vertex;
var writeAtom = true;
if (vPrevIdx >= 0) {
if (walk.numBranches(vPrevIdx) > 1) {
if (this.atoms[vPrevIdx].branch_cnt > 0 && this.atoms[vPrevIdx].paren_written)
this.smiles += ')';
}
openingCycles = walk.numOpeningCycles(eIdx);
for (j = 0; j < openingCycles; j++) {
for (k = 1; k < cycleNumbers.length; k++) {
if (cycleNumbers[k] == -1) // eslint-disable-line max-depth
break;
}
if (k == cycleNumbers.length)
cycleNumbers.push(vPrevIdx);
else
cycleNumbers[k] = vPrevIdx;
this.writeCycleNumber(k);
}
if (vPrevIdx >= 0) {
var branches = walk.numBranches(vPrevIdx);
if (branches > 1 && this.atoms[vPrevIdx].branch_cnt < branches - 1) {
if (walk.edgeClosingCycle(eIdx)) { // eslint-disable-line max-depth
this.atoms[vPrevIdx].paren_written = false;
} else {
this.smiles += '(';
this.atoms[vPrevIdx].paren_written = true;
}
}
this.atoms[vPrevIdx].branch_cnt++;
if (this.atoms[vPrevIdx].branch_cnt > branches)
throw new Error('unexpected branch');
}
var bond = molecule.bonds.get(eIdx);
var dir = 0;
if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE)
dir = this.calcBondDirection(molecule, eIdx, vPrevIdx);
if ((dir == 1 && vIdx == bond.end) || (dir == 2 && vIdx == bond.begin))
this.smiles += '/';
else if ((dir == 2 && vIdx == bond.end) || (dir == 1 && vIdx == bond.begin))
this.smiles += '\\';
else if (bond.type == Struct.Bond.PATTERN.TYPE.ANY)
this.smiles += '~';
else if (bond.type == Struct.Bond.PATTERN.TYPE.DOUBLE)
this.smiles += '=';
else if (bond.type == Struct.Bond.PATTERN.TYPE.TRIPLE)
this.smiles += '#';
else if (bond.type == Struct.Bond.PATTERN.TYPE.AROMATIC &&
(!this.atoms[bond.begin].lowercase || !this.atoms[bond.end].lowercase || !this.isBondInRing(eIdx)))
this.smiles += ':'; // TODO: Check if this : is needed
else if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE && this.atoms[bond.begin].aromatic && this.atoms[bond.end].aromatic)
this.smiles += '-';
if (walk.edgeClosingCycle(eIdx)) {
for (j = 1; j < cycleNumbers.length; j++) {
if (cycleNumbers[j] == vIdx)
break;
}
if (j == cycleNumbers.length)
throw new Error('cycle number not found');
this.writeCycleNumber(j);
cycleNumbers[j] = -1;
writeAtom = false;
}
} else {
if (!firstComponent) {
this.smiles += (this.writtenComponents === walk.nComponentsInReactants &&
walk.nReactants !== 0) ? '>>' : '.'; // when walk.nReactants === 0 - not reaction
}
firstComponent = false;
this.writtenComponents++;
}
if (writeAtom) {
this.writeAtom(molecule, vIdx, this.atoms[vIdx].aromatic, this.atoms[vIdx].lowercase, this.atoms[vIdx].chirality);
this.writtenAtoms.push(seqEl.idx);
}
}
this.comma = false;
// this._writeStereogroups(mol, atoms);
this.writeRadicals(molecule);
// this._writePseudoAtoms(mol);
// this._writeHighlighting();
if (this.comma)
this.smiles += '|';
return this.smiles;
};
Smiles.prototype.writeCycleNumber = function (n) {
if (n > 0 && n < 10)
this.smiles += n;
else if (n >= 10 && n < 100)
this.smiles += '%' + n;
else if (n >= 100 && n < 1000)
this.smiles += '%%' + n;
else
throw new Error('bad cycle number: ' + n);
};
Smiles.prototype.writeAtom = function (mol, idx, aromatic, lowercase, chirality) { // eslint-disable-line max-params, max-statements
var atom = mol.atoms.get(idx);
var needBrackets = false;
var hydro = -1;
var aam = 0;
/*
if (mol.haveQueryAtoms())
{
query_atom = &mol.getQueryAtom(idx);
if (query_atom->type == QUERY_ATOM_RGROUP)
{
if (mol.getRGroups()->isRGroupAtom(idx))
{
const Array<int> &rg = mol.getRGroups()->getSiteRGroups(idx);
if (rg.length != 1)
throw Error("rgroup count %d", rg.length);
_output.printf("[&%d]", rg[0] + 1);
}
else
_output.printf("[&%d]", 1);
return;
}
}
*/
if (atom.label == 'A') {
this.smiles += '*';
return;
}
if (atom.label == 'R' || atom.label == 'R#') {
this.smiles += '[*]';
return;
}
// KETCHER-598 (Ketcher does not save AAM into reaction SMILES)
// BEGIN
// if (this.atom_atom_mapping)
// aam = atom_atom_mapping[idx];
aam = atom.aam;
// END
if (atom.label != 'C' && atom.label != 'P' &&
atom.label != 'N' && atom.label != 'S' &&
atom.label != 'O' && atom.label != 'Cl' &&
atom.label != 'F' && atom.label != 'Br' &&
atom.label != 'B' && atom.label != 'I')
needBrackets = true;
if (atom.explicitValence >= 0 || atom.radical != 0 || chirality > 0 ||
(aromatic && atom.label != 'C' && atom.label != 'O') ||
(aromatic && atom.label == 'C' && this.atoms[idx].neighbours.length < 3 && this.atoms[idx].h_count == 0))
hydro = this.atoms[idx].h_count;
var label = atom.label;
if (atom.atomList && !atom.atomList.notList) {
label = atom.atomList.label();
needBrackets = false; // atom list label already has brackets
} else if (atom.isPseudo() || (atom.atomList && atom.atomList.notList)) {
label = '*';
needBrackets = true;
} else if (chirality || atom.charge != 0 || atom.isotope > 0 || hydro >= 0 || aam > 0) {
needBrackets = true;
}
if (needBrackets) {
if (hydro == -1)
hydro = this.atoms[idx].h_count;
this.smiles += '[';
}
if (atom.isotope > 0)
this.smiles += atom.isotope;
if (lowercase)
this.smiles += label.toLowerCase();
else
this.smiles += label;
if (chirality > 0) {
if (chirality == 1)
this.smiles += '@';
else // chirality == 2
this.smiles += '@@';
if (atom.implicitH > 1)
throw new Error(atom.implicitH + ' implicit H near stereocenter');
}
if (atom.label != 'H') {
if (hydro > 1 || (hydro == 0 && !needBrackets))
this.smiles += 'H' + hydro;
else if (hydro == 1)
this.smiles += 'H';
}
if (atom.charge > 1)
this.smiles += '+' + atom.charge;
else if (atom.charge < -1)
this.smiles += atom.charge;
else if (atom.charge == 1)
this.smiles += '+';
else if (atom.charge == -1)
this.smiles += '-';
if (aam > 0)
this.smiles += ':' + aam;
if (needBrackets)
this.smiles += ']';
/*
if (mol.getRGroupFragment() != 0)
{
for (i = 0; i < 2; i++)
{
int j;
for (j = 0; mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1; j++)
if (idx == mol.getRGroupFragment()->getAttachmentPoint(i, j))
{
_output.printf("([*])");
break;
}
if (mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1)
break;
}
}
*/
};
Smiles.prototype.markCisTrans = function (mol) {
this.cis_trans = new CisTrans(mol, function (idx) {
return this.atoms[idx].neighbours;
}, this);
this.cis_trans.build();
this.dbonds = new Array(mol.bonds.count());
mol.bonds.each(function (bid) {
this.dbonds[bid] =
{
ctbond_beg: -1,
ctbond_end: -1,
saved: 0
};
}, this);
this.cis_trans.each(function (bid, ct) {
var bond = mol.bonds.get(bid);
if (ct.parity != 0 && !this.isBondInRing(bid)) {
var neiBeg = this.atoms[bond.begin].neighbours;
var neiEnd = this.atoms[bond.end].neighbours;
var aromFailBeg = true;
var aromFailEnd = true;
neiBeg.forEach(function (nei) {
if (nei.bid !== bid && mol.bonds.get(nei.bid).type === Struct.Bond.PATTERN.TYPE.SINGLE)
aromFailBeg = false;
}, this);
neiEnd.forEach(function (nei) {
if (nei.bid !== bid && mol.bonds.get(nei.bid).type === Struct.Bond.PATTERN.TYPE.SINGLE)
aromFailEnd = false;
}, this);
if (aromFailBeg || aromFailEnd)
return;
neiBeg.forEach(function (nei) {
if (nei.bid === bid) return;
if (mol.bonds.get(nei.bid).begin === bond.begin)
this.dbonds[nei.bid].ctbond_beg = bid;
else
this.dbonds[nei.bid].ctbond_end = bid;
}, this);
neiEnd.forEach(function (nei) {
if (nei.bid === bid) return;
if (mol.bonds.get(nei.bid).begin === bond.end)
this.dbonds[nei.bid].ctbond_beg = bid;
else
this.dbonds[nei.bid].ctbond_end = bid;
}, this);
}
}, this);
};
Smiles.prototype.updateSideBonds = function (mol, bondIdx) { // eslint-disable-line max-statements
var bond = mol.bonds.get(bondIdx);
var subst = this.cis_trans.getSubstituents(bondIdx);
var parity = this.cis_trans.getParity(bondIdx);
var sidebonds = [-1, -1, -1, -1];
sidebonds[0] = mol.findBondId(subst[0], bond.begin);
if (subst[1] != -1)
sidebonds[1] = mol.findBondId(subst[1], bond.begin);
sidebonds[2] = mol.findBondId(subst[2], bond.end);
if (subst[3] != -1)
sidebonds[3] = mol.findBondId(subst[3], bond.end);
var n1 = 0;
var n2 = 0;
var n3 = 0;
var n4 = 0;
if (this.dbonds[sidebonds[0]].saved != 0) {
if ((this.dbonds[sidebonds[0]].saved == 1 && mol.bonds.get(sidebonds[0]).begin == bond.begin) ||
(this.dbonds[sidebonds[0]].saved == 2 && mol.bonds.get(sidebonds[0]).end == bond.begin))
n1++;
else
n2++;
}
if (sidebonds[1] != -1 && this.dbonds[sidebonds[1]].saved != 0) {
if ((this.dbonds[sidebonds[1]].saved == 2 && mol.bonds.get(sidebonds[1]).begin == bond.begin) ||
(this.dbonds[sidebonds[1]].saved == 1 && mol.bonds.get(sidebonds[1]).end == bond.begin))
n1++;
else
n2++;
}
if (this.dbonds[sidebonds[2]].saved != 0) {
if ((this.dbonds[sidebonds[2]].saved == 1 && mol.bonds.get(sidebonds[2]).begin == bond.end) ||
(this.dbonds[sidebonds[2]].saved == 2 && mol.bonds.get(sidebonds[2]).end == bond.end))
n3++;
else
n4++;
}
if (sidebonds[3] != -1 && this.dbonds[sidebonds[3]].saved != 0) {
if ((this.dbonds[sidebonds[3]].saved == 2 && mol.bonds.get(sidebonds[3]).begin == bond.end) ||
(this.dbonds[sidebonds[3]].saved == 1 && mol.bonds.get(sidebonds[3]).end == bond.end))
n3++;
else
n4++;
}
if (parity == CisTrans.PARITY.CIS) {
n1 += n3;
n2 += n4;
} else {
n1 += n4;
n2 += n3;
}
if (n1 > 0 && n2 > 0)
throw new Error('incompatible cis-trans configuration');
if (n1 == 0 && n2 == 0)
return false;
if (n1 > 0) {
this.dbonds[sidebonds[0]].saved =
(mol.bonds.get(sidebonds[0]).begin == bond.begin) ? 1 : 2;
if (sidebonds[1] != -1) {
this.dbonds[sidebonds[1]].saved =
(mol.bonds.get(sidebonds[1]).begin == bond.begin) ? 2 : 1;
}
this.dbonds[sidebonds[2]].saved =
((mol.bonds.get(sidebonds[2]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 1 : 2;
if (sidebonds[3] != -1) {
this.dbonds[sidebonds[3]].saved =
((mol.bonds.get(sidebonds[3]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 2 : 1;
}
}
if (n2 > 0) {
this.dbonds[sidebonds[0]].saved =
(mol.bonds.get(sidebonds[0]).begin == bond.begin) ? 2 : 1;
if (sidebonds[1] != -1) {
this.dbonds[sidebonds[1]].saved =
(mol.bonds.get(sidebonds[1]).begin == bond.begin) ? 1 : 2;
}
this.dbonds[sidebonds[2]].saved =
((mol.bonds.get(sidebonds[2]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 2 : 1;
if (sidebonds[3] != -1) {
this.dbonds[sidebonds[3]].saved =
((mol.bonds.get(sidebonds[3]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 1 : 2;
}
}
return true;
};
Smiles.prototype.calcBondDirection = function (mol, idx, vprev) {
var ntouched;
if (this.dbonds[idx].ctbond_beg == -1 && this.dbonds[idx].ctbond_end == -1)
return 0;
if (mol.bonds.get(idx).type != Struct.Bond.PATTERN.TYPE.SINGLE)
throw new Error('internal: directed bond type ' + mol.bonds.get(idx).type);
while (true) { // eslint-disable-line no-constant-condition
ntouched = 0;
this.cis_trans.each(function (bid, ct) {
if (ct.parity != 0 && !this.isBondInRing(bid)) {
if (this.updateSideBonds(mol, bid))
ntouched++;
}
}, this);
if (ntouched == this.touchedCistransbonds)
break;
this.touchedCistransbonds = ntouched;
}
if (this.dbonds[idx].saved == 0) {
if (vprev == mol.bonds.get(idx).begin)
this.dbonds[idx].saved = 1;
else
this.dbonds[idx].saved = 2;
}
return this.dbonds[idx].saved;
};
Smiles.prototype.writeRadicals = function (mol) { // eslint-disable-line max-statements
var marked = new Array(this.writtenAtoms.length);
var i, j;
for (i = 0; i < this.writtenAtoms.length; i++) {
if (marked[i])
continue; // eslint-disable-line no-continue
var radical = mol.atoms.get(this.writtenAtoms[i]).radical;
if (radical == 0)
continue; // eslint-disable-line no-continue
if (this.comma) {
this.smiles += ',';
} else {
this.smiles += ' |';
this.comma = true;
}
if (radical == Struct.Atom.PATTERN.RADICAL.SINGLET)
this.smiles += '^3:';
else if (radical == Struct.Atom.PATTERN.RADICAL.DOUPLET)
this.smiles += '^1:';
else // RADICAL_TRIPLET
this.smiles += '^4:';
this.smiles += i;
for (j = i + 1; j < this.writtenAtoms.length; j++) {
if (mol.atoms.get(this.writtenAtoms[j]).radical == radical) {
marked[j] = true;
this.smiles += ',' + j;
}
}
}
};
module.exports = {
stringify: function (struct, options) {
var opts = options || {};
return new Smiles().saveMolecule(struct, opts.ignoreErrors);
}
};

View File

@ -0,0 +1,519 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Map = require('../../util/map');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Struct = require('../struct');
function Stereocenters(mol, neighborsFunc, context) {
this.molecule = mol;
this.atoms = new Map();
this.getNeighbors = neighborsFunc;
this.context = context;
}
Stereocenters.prototype.each = function (func, context) {
this.atoms.each(func, context);
};
Stereocenters.prototype.buildFromBonds = function (/* const int *atom_types, const int *atom_groups, const int *bond_orientations, */ignoreErrors) {
var atoms = this.molecule.atoms;
var bonds = this.molecule.bonds;
// this is a set of atoms that are likely to belong to allene structures and
// therefore should not be considered as potential stereocenters in the code below,
// as allenes cannot be encoded in the SMILES notation
var alleneMask = Set.empty();
atoms.each(function (aid) {
var neiList = this.getNeighbors.call(this.context, aid);
if (neiList.length != 2)
return false;
var nei1 = neiList[0];
var nei2 = neiList[1];
// check atom labels
if ([aid, nei1.aid, nei2.aid].findIndex(function (aid) {
return ['C', 'Si'].indexOf(atoms.get(aid).label) < 0;
}, this) >= 0)
return false;
// check adjacent bond types
if ([nei1.bid, nei2.bid].findIndex(function (bid) {
return bonds.get(bid).type != Struct.Bond.PATTERN.TYPE.DOUBLE;
}, this) >= 0)
return false;
// get the other neighbors of the two adjacent atoms except for the central atom
var nei1nei = this.getNeighbors.call(this.context, nei1.aid).filter(function (nei) {
return nei.aid != aid;
});
var nei2nei = this.getNeighbors.call(this.context, nei2.aid).filter(function (nei) {
return nei.aid != aid;
});
if (nei1nei.length < 1 || nei1nei.length > 2 || nei2nei.length < 1 || nei2nei.length > 2)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE;
}, this) >= 0)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).stereo == Struct.Bond.PATTERN.STEREO.EITHER;
}, this) >= 0)
return false;
Set.add(alleneMask, nei1.aid);
Set.add(alleneMask, nei2.aid);
}, this);
if (Set.size(alleneMask) > 0)
alert('This structure may contain allenes, which cannot be represented in the SMILES notation. Relevant stereo-information will be discarded.');
atoms.each(function (aid) {
if (Set.contains(alleneMask, aid))
return;
/*
if (atom_types[atom_idx] == 0)
continue;
*/
var neiList = this.getNeighbors.call(this.context, aid);
var stereocenter = false;
neiList.find(function (nei) {
var bond = this.molecule.bonds.get(nei.bid);
if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE && bond.begin == aid) {
if (bond.stereo == Struct.Bond.PATTERN.STEREO.UP || bond.stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
stereocenter = true;
return true;
}
}
return false;
}, this);
if (!stereocenter)
return;
if (ignoreErrors)
// try
// {
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
// }
// catch (er)
// {
// }
else
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
}, this);
};
Stereocenters.allowed_stereocenters =
[
{ elem: 'C', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'C', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 4, n_double_bonds: 2, implicit_degree: 4 },
{ elem: 'S', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 3, n_double_bonds: 1, implicit_degree: 3 },
{ elem: 'P', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'P', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'P', charge: 0, degree: 4, n_double_bonds: 1, implicit_degree: 4 }
];
Stereocenters.prototype.buildOneCenter = function (atomIdx/* , int group, int type, const int *bond_orientations*/) { // eslint-disable-line max-statements
var atom = this.molecule.atoms.get(atomIdx);
var neiList = this.getNeighbors.call(this.context, atomIdx);
var degree = neiList.length;
var implicitDegree = -1;
var stereocenter = {
group: 0, // = group;
type: 0, // = type;
pyramid: []
};
var edgeIds = [];
var lastAtomDir = 0;
var nDoubleBonds = 0;
stereocenter.pyramid[0] = -1;
stereocenter.pyramid[1] = -1;
stereocenter.pyramid[2] = -1;
stereocenter.pyramid[3] = -1;
var nPureHydrogens = 0;
if (degree > 4)
throw new Error('stereocenter with %d bonds are not supported' + degree);
neiList.forEach(function (nei, neiIdx) {
var neiAtom = this.molecule.atoms.get(nei.aid);
var bond = this.molecule.bonds.get(nei.bid);
edgeIds[neiIdx] = {
edge_idx: nei.bid,
nei_idx: nei.aid,
rank: nei.aid,
vec: Vec2.diff(neiAtom.pp, atom.pp).yComplement()
};
if (neiAtom.pureHydrogen()) {
nPureHydrogens++;
edgeIds[neiIdx].rank = 10000;
} else if (neiAtom.label === 'H') {
edgeIds[neiIdx].rank = 5000;
}
if (!edgeIds[neiIdx].vec.normalize())
throw new Error('zero bond length');
if (bond.type === Struct.Bond.PATTERN.TYPE.TRIPLE)
throw new Error('non-single bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.AROMATIC)
throw new Error('aromatic bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.DOUBLE)
nDoubleBonds++;
}, this);
Stereocenters.allowed_stereocenters.find(function (as) {
if (as.elem == atom.label && as.charge == atom.charge &&
as.degree == degree && as.n_double_bonds == nDoubleBonds) {
implicitDegree = as.implicit_degree;
return true;
}
return false;
}, this);
if (implicitDegree === -1)
throw new Error('unknown stereocenter configuration: ' + atom.label + ', charge ' + atom.charge + ', ' + degree + ' bonds (' + nDoubleBonds + ' double)');
if (degree === 4 && nPureHydrogens > 1)
throw new Error(nPureHydrogens + ' hydrogens near stereocenter');
if (degree === 3 && implicitDegree == 4 && nPureHydrogens > 0)
throw new Error('have hydrogen(s) besides implicit hydrogen near stereocenter');
/*
if (stereocenter.type == ATOM_ANY)
{
_stereocenters.insert(atom_idx, stereocenter);
return;
}
*/
if (degree === 4) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[2].rank > edgeIds[3].rank)
swap(edgeIds, 2, 3);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
var main1 = -1;
var main2 = -1;
var side1 = -1;
var side2 = -1;
var mainDir = 0;
for (var neiIdx = 0; neiIdx < 4; neiIdx++) {
var stereo = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (stereo == Struct.Bond.PATTERN.STEREO.UP || stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
main1 = neiIdx;
mainDir = stereo;
break;
}
}
if (main1 == -1)
throw new Error('none of 4 bonds going from stereocenter is stereobond');
var xyz1, xyz2;
// find main2 as opposite to main1
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 1) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 2) % 4;
side1 = (main1 + 1) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 3) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 1) % 4;
}
}
if (main2 == -1)
throw new Error('internal error: can not find opposite bond');
if (mainDir == Struct.Bond.PATTERN.STEREO.UP && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.DOWN)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == Struct.Bond.PATTERN.STEREO.DOWN && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.UP)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side1].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side2].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (main1 == 3 || main2 == 3)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = edgeIds[3].nei_idx;
} else if (degree === 3) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
var stereo0 = this.getBondStereo(atomIdx, edgeIds[0].edge_idx);
var stereo1 = this.getBondStereo(atomIdx, edgeIds[1].edge_idx);
var stereo2 = this.getBondStereo(atomIdx, edgeIds[2].edge_idx);
var nUp = 0;
var nDown = 0;
nUp += (stereo0 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo1 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo2 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nDown += (stereo0 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo1 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo2 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
if (implicitDegree == 4) { // have implicit hydrogen
if (nUp == 3)
throw new Error('all 3 bonds up near stereoatom');
if (nDown == 3)
throw new Error('all 3 bonds down near stereoatom');
if (nUp == 0 && nDown == 0)
throw new Error('no up/down bonds near stereoatom -- indefinite case');
if (nUp == 1 && nDown == 1)
throw new Error('one bond up, one bond down -- indefinite case');
mainDir = 0;
if (nUp == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.DOWN;
} else if (nDown == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.UP;
} else {
main1 = -1;
side1 = -1;
side2 = -1;
for (neiIdx = 0; neiIdx < 3; neiIdx++) {
dir = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (dir == Struct.Bond.PATTERN.STEREO.UP || dir == Struct.Bond.PATTERN.STEREO.DOWN) { // eslint-disable-line max-depth
main1 = neiIdx;
mainDir = dir;
side1 = (neiIdx + 1) % 3;
side2 = (neiIdx + 2) % 3;
break;
}
}
if (main1 == -1)
throw new Error('internal error: can not find up or down bond');
var xyz = Stereocenters.xyzzy(edgeIds[side1].vec, edgeIds[side2].vec, edgeIds[main1].vec);
if (xyz == 3 || xyz == 4)
throw new Error('degenerate case for 3 bonds near stereoatom');
if (xyz == 1)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
}
var sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = -1;
} else { // 3-connected P, N or S; no implicit hydrogens
var dir;
if (nDown > 0 && nUp > 0)
throw new Error('one bond up, one bond down -- indefinite case');
else if (nDown == 0 && nUp == 0)
throw new Error('no up-down bonds attached to stereocenter');
else if (nUp > 0)
dir = 1;
else
dir = -1;
if (Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[2].vec, edgeIds[1].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[2].vec, edgeIds[1].vec, edgeIds[0].vec) === 1)
// all bonds belong to the same half-plane
dir = -dir;
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if (sign == dir) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
}
stereocenter.pyramid[3] = -1;
}
}
this.atoms.set(atomIdx, stereocenter);
};
Stereocenters.prototype.getBondStereo = function (centerIdx, edgeIdx) {
var bond = this.molecule.bonds.get(edgeIdx);
if (centerIdx != bond.begin) // TODO: check this
return 0;
return bond.stereo;
};
// 1 -- in the smaller angle, 2 -- in the bigger angle,
// 4 -- in the 'positive' straight angle, 8 -- in the 'negative' straight angle
Stereocenters.xyzzy = function (v1, v2, u) {
var eps = 0.001;
var sine1 = Vec2.cross(v1, v2);
var cosine1 = Vec2.dot(v1, v2);
var sine2 = Vec2.cross(v1, u);
var cosine2 = Vec2.dot(v1, u);
if (Math.abs(sine1) < eps) {
if (Math.abs(sine2) < eps)
throw new Error('degenerate case -- bonds overlap');
return (sine2 > 0) ? 4 : 8;
}
if (sine1 * sine2 < -eps * eps)
return 2;
if (cosine2 < cosine1)
return 2;
return 1;
};
Stereocenters.sign = function (v1, v2, v3) {
var res = (v1.x - v3.x) * (v2.y - v3.y) - (v1.y - v3.y) * (v2.x - v3.x); // eslint-disable-line no-mixed-operators
var eps = 0.001;
if (res > eps)
return 1;
if (res < -eps)
return -1;
throw new Error('degenerate triangle');
};
Stereocenters.isPyramidMappingRigid = function (mapping) {
var arr = mapping.slice();
var rigid = true;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[2] > arr[3])
swap(arr, 2, 3), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
return rigid;
};
function swap(arr, i1, i2) {
var tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
module.exports = Stereocenters;

View File

@ -0,0 +1,435 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var element = require('../element');
var AtomList = require('./atomlist');
function Atom(params) { // eslint-disable-line max-statements
var def = Atom.attrGetDefault;
console.assert(params || 'label' in params, 'label must be specified!');
this.label = params.label;
this.fragment = ('fragment' in params) ? params.fragment : -1;
this.pseudo = params.pseudo || checkPseudo(params.label);
ifDef(this, params, 'alias', def('alias'));
ifDef(this, params, 'isotope', def('isotope'));
ifDef(this, params, 'radical', def('radical'));
ifDef(this, params, 'charge', def('charge'));
ifDef(this, params, 'rglabel', def('rglabel')); // r-group index mask, i-th bit stands for i-th r-site
ifDef(this, params, 'attpnt', def('attpnt')); // attachment point
ifDef(this, params, 'explicitValence', def('explicitValence'));
this.valence = 0;
this.implicitH = 0; // implicitH is not an attribute
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
// sgs should only be set when an atom is added to an s-group by an appropriate method,
// or else a copied atom might think it belongs to a group, but the group be unaware of the atom
// TODO: make a consistency check on atom/s-group assignments
this.sgs = {};
// query
ifDef(this, params, 'ringBondCount', def('ringBondCount'));
ifDef(this, params, 'substitutionCount', def('substitutionCount'));
ifDef(this, params, 'unsaturatedAtom', def('unsaturatedAtom'));
ifDef(this, params, 'hCount', def('hCount'));
// reaction
ifDef(this, params, 'aam', def('aam'));
ifDef(this, params, 'invRet', def('invRet'));
ifDef(this, params, 'exactChangeFlag', def('exactChangeFlag'));
ifDef(this, params, 'rxnFragmentType', -1); // this isn't really an attribute
this.atomList = params.atomList ? new AtomList(params.atomList) : null;
this.neighbors = []; // set of half-bonds having this atom as their origin
this.badConn = false;
}
Atom.getAttrHash = function (atom) {
var attrs = {};
for (var attr in Atom.attrlist) {
if (typeof (atom[attr]) !== 'undefined')
attrs[attr] = atom[attr];
}
return attrs;
};
Atom.attrGetDefault = function (attr) {
if (attr in Atom.attrlist)
return Atom.attrlist[attr];
console.assert(false, 'Attribute unknown');
};
Atom.PATTERN =
{
RADICAL:
{
NONE: 0,
SINGLET: 1,
DOUPLET: 2,
TRIPLET: 3
}
};
Atom.attrlist = {
alias: null,
label: 'C',
pseudo: null,
isotope: 0,
radical: 0,
charge: 0,
explicitValence: -1,
ringBondCount: 0,
substitutionCount: 0,
unsaturatedAtom: 0,
hCount: 0,
atomList: null,
invRet: 0,
exactChangeFlag: 0,
rglabel: null,
attpnt: null,
aam: 0
};
function radicalElectrons(radical) {
radical -= 0;
if (radical === Atom.PATTERN.RADICAL.NONE)
return 0;
else if (radical === Atom.PATTERN.RADICAL.DOUPLET)
return 1;
else if (radical === Atom.PATTERN.RADICAL.SINGLET ||
radical === Atom.PATTERN.RADICAL.TRIPLET)
return 2;
console.assert(false, 'Unknown radical value');
}
Atom.prototype.clone = function (fidMap) {
var ret = new Atom(this);
if (fidMap && this.fragment in fidMap)
ret.fragment = fidMap[this.fragment];
return ret;
};
Atom.prototype.isQuery = function () {
return this.atomList != null || this.label === 'A' || this.attpnt || this.hCount;
};
Atom.prototype.pureHydrogen = function () {
return this.label === 'H' && this.isotope === 0;
};
Atom.prototype.isPlainCarbon = function () {
return this.label === 'C' && this.isotope === 0 && this.radical == 0 && this.charge == 0 &&
this.explicitValence < 0 && this.ringBondCount == 0 && this.substitutionCount == 0 &&
this.unsaturatedAtom == 0 && this.hCount == 0 && !this.atomList;
};
Atom.prototype.isPseudo = function () {
// TODO: handle reaxys generics separately
return !this.atomList && !this.rglabel && !element.map[this.label];
};
Atom.prototype.hasRxnProps = function () {
return !!(this.invRet || this.exactChangeFlag || this.attpnt != null || this.aam);
};
Atom.prototype.calcValence = function (conn) { // eslint-disable-line max-statements
var atom = this;
var charge = atom.charge;
var label = atom.label;
if (atom.isQuery()) {
this.implicitH = 0;
return true;
}
var elem = element.map[label];
if (elem === undefined) {
this.implicitH = 0;
return true;
}
var groupno = element[elem].group;
var rad = radicalElectrons(atom.radical);
var valence = conn;
var hyd = 0;
var absCharge = Math.abs(charge);
if (groupno === 1) {
if (label === 'H' ||
label === 'Li' || label === 'Na' || label === 'K' ||
label === 'Rb' || label === 'Cs' || label === 'Fr') {
valence = 1;
hyd = 1 - rad - conn - absCharge;
}
} else if (groupno === 2) {
if (conn + rad + absCharge === 2 || conn + rad + absCharge === 0)
valence = 2;
else
hyd = -1;
} else if (groupno === 3) {
if (label === 'B' || label === 'Al' || label === 'Ga' || label === 'In') {
if (charge === -1) {
valence = 4;
hyd = 4 - rad - conn;
} else {
valence = 3;
hyd = 3 - rad - conn - absCharge;
}
} else if (label === 'Tl') {
if (charge === -1) {
if (rad + conn <= 2) {
valence = 2;
hyd = 2 - rad - conn;
} else {
valence = 4;
hyd = 4 - rad - conn;
}
} else if (charge === -2) {
if (rad + conn <= 3) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 5;
hyd = 5 - rad - conn;
}
} else if (rad + conn + absCharge <= 1) {
valence = 1;
hyd = 1 - rad - conn - absCharge;
} else {
valence = 3;
hyd = 3 - rad - conn - absCharge;
}
}
} else if (groupno === 4) {
if (label === 'C' || label === 'Si' || label === 'Ge') {
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else if (label === 'Sn' || label === 'Pb') {
if (conn + rad + absCharge <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else {
valence = 4;
hyd = 4 - rad - conn - absCharge;
}
}
} else if (groupno === 5) {
if (label === 'N' || label === 'P') {
if (charge === 1) {
valence = 4;
hyd = 4 - rad - conn;
} else if (charge === 2) {
valence = 3;
hyd = 3 - rad - conn;
} else if (label === 'N' || rad + conn + absCharge <= 3) {
valence = 3;
hyd = 3 - rad - conn - absCharge;
} else { // ELEM_P && rad + conn + absCharge > 3
valence = 5;
hyd = 5 - rad - conn - absCharge;
}
} else if (label === 'Bi' || label === 'Sb' || label === 'As') {
if (charge === 1) {
if (rad + conn <= 2 && label !== 'As') {
valence = 2;
hyd = 2 - rad - conn;
} else {
valence = 4;
hyd = 4 - rad - conn;
}
} else if (charge === 2) {
valence = 3;
hyd = 3 - rad - conn;
} else if (rad + conn <= 3) {
valence = 3;
hyd = 3 - rad - conn - absCharge;
} else {
valence = 5;
hyd = 5 - rad - conn - absCharge;
}
}
} else if (groupno === 6) {
if (label === 'O') {
if (charge >= 1) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 2;
hyd = 2 - rad - conn - absCharge;
}
} else if (label === 'S' || label === 'Se' || label === 'Po') {
if (charge === 1) {
if (conn <= 3) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 5;
hyd = 5 - rad - conn;
}
} else if (conn + rad + absCharge <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else if (conn + rad + absCharge <= 4) {
// See examples in PubChem
// [S] : CID 16684216
// [Se]: CID 5242252
// [Po]: no example, just following ISIS/Draw logic here
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else {
// See examples in PubChem
// [S] : CID 46937044
// [Se]: CID 59786
// [Po]: no example, just following ISIS/Draw logic here
valence = 6;
hyd = 6 - rad - conn - absCharge;
}
} else if (label === 'Te') {
if (charge === -1) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
}
} else if (charge === 0 || charge === 2) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else if (conn <= 4) {
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else if (charge === 0 && conn <= 6) {
valence = 6;
hyd = 6 - rad - conn - absCharge;
} else {
hyd = -1;
}
}
}
} else if (groupno === 7) {
if (label === 'F') {
valence = 1;
hyd = 1 - rad - conn - absCharge;
} else if (label === 'Cl' || label === 'Br' ||
label === 'I' || label === 'At') {
if (charge === 1) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn;
} else if (conn === 3 || conn === 5 || conn >= 7) {
hyd = -1;
}
} else if (charge === 0) {
if (conn <= 1) {
valence = 1;
hyd = 1 - rad - conn;
// While the halogens can have valence 3, they can not have
// hydrogens in that case.
} else if (conn === 2 || conn === 4 || conn === 6) {
if (rad === 1) {
valence = conn;
hyd = 0;
} else {
hyd = -1; // will throw an error in the end
}
} else if (conn > 7) {
hyd = -1; // will throw an error in the end
}
}
}
} else if (groupno === 8) {
if (conn + rad + absCharge === 0)
valence = 1;
else
hyd = -1;
}
this.valence = valence;
this.implicitH = hyd;
if (this.implicitH < 0) {
this.valence = conn;
this.implicitH = 0;
this.badConn = true;
return false;
}
return true;
};
Atom.prototype.calcValenceMinusHyd = function (conn) { // eslint-disable-line max-statements
var atom = this;
var charge = atom.charge;
var label = atom.label;
var elem = element.map[label];
if (elem === null)
console.assert('Element ' + label + ' unknown');
if (elem < 0) { // query atom, skip
this.implicitH = 0;
return null;
}
var groupno = element[elem].group;
var rad = radicalElectrons(atom.radical);
if (groupno === 3) {
if (label === 'B' || label === 'Al' || label === 'Ga' || label === 'In') {
if (charge === -1) {
if (rad + conn <= 4)
return rad + conn;
}
}
} else if (groupno === 5) {
if (label === 'N' || label === 'P') {
if (charge === 1)
return rad + conn;
if (charge === 2)
return rad + conn;
} else if (label === 'Sb' || label === 'Bi' || label === 'As') {
if (charge === 1)
return rad + conn;
else if (charge === 2)
return rad + conn;
}
} else if (groupno === 6) {
if (label === 'O') {
if (charge >= 1)
return rad + conn;
} else if (label === 'S' || label === 'Se' || label === 'Po') {
if (charge === 1)
return rad + conn;
}
} else if (groupno === 7) {
if (label === 'Cl' || label === 'Br' ||
label === 'I' || label === 'At') {
if (charge === 1)
return rad + conn;
}
}
return rad + conn + Math.abs(charge);
};
function ifDef(dst, src, prop, def) {
dst[prop] = !(typeof src[prop] === 'undefined') ? src[prop] : def;
}
function checkPseudo(label) {
return !element.map[label] && label !== 'L' && label !== 'L#' && label !== 'R#' ? label : null;
}
module.exports = Atom;

View File

@ -0,0 +1,44 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var element = require('../element');
function AtomList(params) {
console.assert(params && 'notList' in params && 'ids' in params, '\'notList\' and \'ids\' must be specified!');
this.notList = params.notList; /* boolean*/
this.ids = params.ids; /* Array of integers*/
}
AtomList.prototype.labelList = function () {
var labels = [];
for (var i = 0; i < this.ids.length; ++i)
labels.push(element[this.ids[i]].label);
return labels;
};
AtomList.prototype.label = function () {
var label = '[' + this.labelList().join(',') + ']';
if (this.notList)
label = '!' + label;
return label;
};
AtomList.prototype.equals = function (x) {
return this.notList == x.notList && (this.ids || []).sort().toString() === (x.ids || []).sort().toString();
};
module.exports = AtomList;

View File

@ -0,0 +1,145 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Vec2 = require('../../util/vec2');
function Bond(params) { // eslint-disable-line max-statements
console.assert(params && 'begin' in params && 'end' in params && 'type' in params,
'\'begin\', \'end\' and \'type\' properties must be specified!');
this.begin = params.begin;
this.end = params.end;
this.type = params.type;
this.xxx = params.xxx || '';
this.stereo = Bond.PATTERN.STEREO.NONE;
this.topology = Bond.PATTERN.TOPOLOGY.EITHER;
this.reactingCenterStatus = 0;
this.hb1 = null; // half-bonds
this.hb2 = null;
this.len = 0;
this.sb = 0;
this.sa = 0;
this.angle = 0;
if (params.stereo)
this.stereo = params.stereo;
if (params.topology)
this.topology = params.topology;
if (params.reactingCenterStatus)
this.reactingCenterStatus = params.reactingCenterStatus;
this.center = new Vec2();
}
Bond.PATTERN =
{
TYPE:
{
SINGLE: 1,
DOUBLE: 2,
TRIPLE: 3,
AROMATIC: 4,
SINGLE_OR_DOUBLE: 5,
SINGLE_OR_AROMATIC: 6,
DOUBLE_OR_AROMATIC: 7,
ANY: 8
},
STEREO:
{
NONE: 0,
UP: 1,
EITHER: 4,
DOWN: 6,
CIS_TRANS: 3
},
TOPOLOGY:
{
EITHER: 0,
RING: 1,
CHAIN: 2
},
REACTING_CENTER:
{
NOT_CENTER: -1,
UNMARKED: 0,
CENTER: 1,
UNCHANGED: 2,
MADE_OR_BROKEN: 4,
ORDER_CHANGED: 8,
MADE_OR_BROKEN_AND_CHANGED: 12
}
};
Bond.attrlist = {
type: Bond.PATTERN.TYPE.SINGLE,
stereo: Bond.PATTERN.STEREO.NONE,
topology: Bond.PATTERN.TOPOLOGY.EITHER,
reactingCenterStatus: 0
};
// TODO: not used
Bond.getAttrHash = function (bond) {
var attrs = {};
for (var attr in Bond.attrlist) {
if (typeof (bond[attr]) !== 'undefined')
attrs[attr] = bond[attr];
}
return attrs;
};
Bond.attrGetDefault = function (attr) {
if (attr in Bond.attrlist)
return Bond.attrlist[attr];
console.error('Attribute unknown');
};
Bond.prototype.hasRxnProps = function () {
return !!this.reactingCenterStatus;
};
Bond.prototype.getCenter = function (struct) {
var p1 = struct.atoms.get(this.begin).pp;
var p2 = struct.atoms.get(this.end).pp;
return Vec2.lc2(p1, 0.5, p2, 0.5);
};
Bond.prototype.getDir = function (struct) {
var p1 = struct.atoms.get(this.begin).pp;
var p2 = struct.atoms.get(this.end).pp;
return p2.sub(p1).normalized();
};
Bond.prototype.clone = function (aidMap) {
var cp = new Bond(this);
if (aidMap) {
cp.begin = aidMap[cp.begin];
cp.end = aidMap[cp.end];
}
return cp;
};
Bond.prototype.findOtherEnd = function (i) {
if (i == this.begin)
return this.end;
if (i == this.end)
return this.begin;
console.error('bond end not found');
};
module.exports = Bond;

View File

@ -0,0 +1,975 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Map = require('../../util/map');
var Pool = require('../../util/pool');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var element = require('../element');
var Atom = require('./atom');
var AtomList = require('./atomlist');
var Bond = require('./bond');
var SGroup = require('./sgroup');
var RGroup = require('./rgroup');
var SGroupForest = require('./sgforest');
function Struct() {
this.atoms = new Pool();
this.bonds = new Pool();
this.sgroups = new Pool();
this.halfBonds = new Map();
this.loops = new Pool();
this.isChiral = false;
this.isReaction = false;
this.rxnArrows = new Pool();
this.rxnPluses = new Pool();
this.frags = new Pool();
this.rgroups = new Map();
this.name = '';
this.sGroupForest = new SGroupForest(this);
}
Struct.prototype.hasRxnProps = function () {
return this.atoms.find(function (aid, atom) {
return atom.hasRxnProps();
}, this) >= 0 || this.bonds.find(function (bid, bond) {
return bond.hasRxnProps();
}, this) >= 0;
};
Struct.prototype.hasRxnArrow = function () {
return this.rxnArrows.count() > 0;
};
// returns a list of id's of s-groups, which contain only atoms in the given list
Struct.prototype.getSGroupsInAtomSet = function (atoms/* Array*/) {
var sgCounts = {};
atoms.forEach(function (aid) {
var sg = Set.list(this.atoms.get(aid).sgs);
sg.forEach(function (sid) {
sgCounts[sid] = sgCounts[sid] ? (sgCounts[sid] + 1) : 1;
}, this);
}, this);
var sgroupList = [];
for (var key in sgCounts) {
var sid = parseInt(key, 10);
var sgroup = this.sgroups.get(sid);
var sgAtoms = SGroup.getAtoms(this, sgroup);
if (sgCounts[key] === sgAtoms.length)
sgroupList.push(sid);
}
return sgroupList;
};
Struct.prototype.isBlank = function () {
return this.atoms.count() === 0 &&
this.rxnArrows.count() === 0 &&
this.rxnPluses.count() === 0 && !this.isChiral;
};
Struct.prototype.toLists = function () {
var aidMap = {};
var atomList = [];
this.atoms.each(function (aid, atom) {
aidMap[aid] = atomList.length;
atomList.push(atom);
});
var bondList = [];
this.bonds.each(function (bid, bond) {
var b = new Bond(bond);
b.begin = aidMap[bond.begin];
b.end = aidMap[bond.end];
bondList.push(b);
});
return {
atoms: atomList,
bonds: bondList
};
};
Struct.prototype.clone = function (atomSet, bondSet, dropRxnSymbols, aidMap) {
var cp = new Struct();
return this.mergeInto(cp, atomSet, bondSet, dropRxnSymbols, false, aidMap);
};
Struct.prototype.getScaffold = function () {
var atomSet = Set.empty();
this.atoms.each(function (aid) {
Set.add(atomSet, aid);
}, this);
this.rgroups.each(function (rgid, rg) {
rg.frags.each(function (fnum, fid) {
this.atoms.each(function (aid, atom) {
if (atom.fragment === fid)
Set.remove(atomSet, aid);
}, this);
}, this);
}, this);
return this.clone(atomSet);
};
Struct.prototype.getFragmentIds = function (fid) {
const atomSet = Set.empty();
this.atoms.each((aid, atom) => {
if (atom.fragment === fid)
Set.add(atomSet, aid);
});
return atomSet;
};
Struct.prototype.getFragment = function (fid) {
return this.clone(this.getFragmentIds(fid));
};
Struct.prototype.mergeInto = function (cp, atomSet, bondSet, dropRxnSymbols, keepAllRGroups, aidMap) { // eslint-disable-line max-params, max-statements
atomSet = atomSet || Set.keySetInt(this.atoms);
bondSet = bondSet || Set.keySetInt(this.bonds);
bondSet = Set.filter(bondSet, function (bid) {
var bond = this.bonds.get(bid);
return Set.contains(atomSet, bond.begin) && Set.contains(atomSet, bond.end);
}, this);
var fidMask = {};
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid))
fidMask[atom.fragment] = 1;
});
var fidMap = {};
this.frags.each(function (fid, frag) {
if (fidMask[fid])
fidMap[fid] = cp.frags.add(Object.assign({}, frag));
});
var rgroupsIds = [];
this.rgroups.each(function (rgid, rgroup) {
var keepGroup = keepAllRGroups;
if (!keepGroup) {
rgroup.frags.each(function (fnum, fid) {
rgroupsIds.push(fid);
if (fidMask[fid])
keepGroup = true;
});
if (!keepGroup)
return;
}
var rg = cp.rgroups.get(rgid);
if (rg) {
rgroup.frags.each(function (fnum, fid) {
rgroupsIds.push(fid);
if (fidMask[fid])
rg.frags.add(fidMap[fid]);
});
} else {
cp.rgroups.set(rgid, rgroup.clone(fidMap));
}
});
if (typeof aidMap === 'undefined' || aidMap === null)
aidMap = {};
// atoms in not RGroup
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid) && rgroupsIds.indexOf(atom.fragment) === -1)
aidMap[aid] = cp.atoms.add(atom.clone(fidMap));
});
// atoms in RGroup
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid) && rgroupsIds.indexOf(atom.fragment) !== -1)
aidMap[aid] = cp.atoms.add(atom.clone(fidMap));
});
var bidMap = {};
this.bonds.each(function (bid, bond) {
if (Set.contains(bondSet, bid))
bidMap[bid] = cp.bonds.add(bond.clone(aidMap));
});
this.sgroups.each(function (sid, sg) {
var i;
for (i = 0; i < sg.atoms.length; ++i) {
if (!Set.contains(atomSet, sg.atoms[i]))
return;
}
sg = SGroup.clone(sg, aidMap, bidMap);
var id = cp.sgroups.add(sg);
sg.id = id;
for (i = 0; i < sg.atoms.length; ++i)
Set.add(cp.atoms.get(sg.atoms[i]).sgs, id);
if (sg.type === 'DAT')
cp.sGroupForest.insert(sg.id, -1, []);
else
cp.sGroupForest.insert(sg.id);
});
cp.isChiral = this.isChiral;
if (!dropRxnSymbols) {
cp.isReaction = this.isReaction;
this.rxnArrows.each(function (id, item) {
cp.rxnArrows.add(item.clone());
});
this.rxnPluses.each(function (id, item) {
cp.rxnPluses.add(item.clone());
});
}
return cp;
};
Struct.prototype.findBondId = function (begin, end) {
var id = -1;
this.bonds.find(function (bid, bond) {
if ((bond.begin === begin && bond.end === end) ||
(bond.begin === end && bond.end === begin)) {
id = bid;
return true;
}
return false;
}, this);
return id;
};
function HalfBond(/* num*/begin, /* num*/end, /* num*/bid) { // eslint-disable-line max-params, max-statements
console.assert(arguments.length === 3, 'Invalid parameter number!');
this.begin = begin - 0;
this.end = end - 0;
this.bid = bid - 0;
// rendering properties
this.dir = new Vec2(); // direction
this.norm = new Vec2(); // left normal
this.ang = 0; // angle to (1,0), used for sorting the bonds
this.p = new Vec2(); // corrected origin position
this.loop = -1; // left loop id if the half-bond is in a loop, otherwise -1
this.contra = -1; // the half bond contrary to this one
this.next = -1; // the half-bond next ot this one in CCW order
this.leftSin = 0;
this.leftCos = 0;
this.leftNeighbor = 0;
this.rightSin = 0;
this.rightCos = 0;
this.rightNeighbor = 0;
}
Struct.prototype.initNeighbors = function () {
this.atoms.each(function (aid, atom) {
atom.neighbors = [];
});
this.bonds.each(function (bid, bond) {
var a1 = this.atoms.get(bond.begin);
var a2 = this.atoms.get(bond.end);
a1.neighbors.push(bond.hb1);
a2.neighbors.push(bond.hb2);
}, this);
};
Struct.prototype.bondInitHalfBonds = function (bid, /* opt*/ bond) {
bond = bond || this.bonds.get(bid);
bond.hb1 = 2 * bid;
bond.hb2 = 2 * bid + 1; // eslint-disable-line no-mixed-operators
this.halfBonds.set(bond.hb1, new HalfBond(bond.begin, bond.end, bid));
this.halfBonds.set(bond.hb2, new HalfBond(bond.end, bond.begin, bid));
var hb1 = this.halfBonds.get(bond.hb1);
var hb2 = this.halfBonds.get(bond.hb2);
hb1.contra = bond.hb2;
hb2.contra = bond.hb1;
};
Struct.prototype.halfBondUpdate = function (hbid) {
var hb = this.halfBonds.get(hbid);
var p1 = this.atoms.get(hb.begin).pp;
var p2 = this.atoms.get(hb.end).pp;
var d = Vec2.diff(p2, p1).normalized();
hb.dir = Vec2.dist(p2, p1) > 1e-4 ? d : new Vec2(1, 0);
hb.norm = hb.dir.turnLeft();
hb.ang = hb.dir.oxAngle();
if (hb.loop < 0)
hb.loop = -1;
};
Struct.prototype.initHalfBonds = function () {
this.halfBonds.clear();
this.bonds.each(this.bondInitHalfBonds, this);
};
Struct.prototype.setHbNext = function (hbid, next) {
this.halfBonds.get(this.halfBonds.get(hbid).contra).next = next;
};
Struct.prototype.halfBondSetAngle = function (hbid, left) {
var hb = this.halfBonds.get(hbid);
var hbl = this.halfBonds.get(left);
hbl.rightCos = hb.leftCos = Vec2.dot(hbl.dir, hb.dir);
hbl.rightSin = hb.leftSin = Vec2.cross(hbl.dir, hb.dir);
hb.leftNeighbor = left;
hbl.rightNeighbor = hbid;
};
Struct.prototype.atomAddNeighbor = function (hbid) {
var hb = this.halfBonds.get(hbid);
var atom = this.atoms.get(hb.begin);
var i = 0;
for (i = 0; i < atom.neighbors.length; ++i) {
if (this.halfBonds.get(atom.neighbors[i]).ang > hb.ang)
break;
}
atom.neighbors.splice(i, 0, hbid);
var ir = atom.neighbors[(i + 1) % atom.neighbors.length];
var il = atom.neighbors[(i + atom.neighbors.length - 1) %
atom.neighbors.length];
this.setHbNext(il, hbid);
this.setHbNext(hbid, ir);
this.halfBondSetAngle(hbid, il);
this.halfBondSetAngle(ir, hbid);
};
Struct.prototype.atomSortNeighbors = function (aid) {
var atom = this.atoms.get(aid);
var halfBonds = this.halfBonds;
atom.neighbors = atom.neighbors.sort(function (nei, nei2) {
return halfBonds.get(nei).ang - halfBonds.get(nei2).ang;
});
var i;
for (i = 0; i < atom.neighbors.length; ++i) {
this.halfBonds.get(this.halfBonds.get(atom.neighbors[i]).contra).next =
atom.neighbors[(i + 1) % atom.neighbors.length];
}
for (i = 0; i < atom.neighbors.length; ++i) {
this.halfBondSetAngle(atom.neighbors[(i + 1) % atom.neighbors.length],
atom.neighbors[i]);
}
};
Struct.prototype.sortNeighbors = function (list) {
function f(aid) {
this.atomSortNeighbors(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.atomUpdateHalfBonds = function (aid) {
var nei = this.atoms.get(aid).neighbors;
for (var i = 0; i < nei.length; ++i) {
var hbid = nei[i];
this.halfBondUpdate(hbid);
this.halfBondUpdate(this.halfBonds.get(hbid).contra);
}
};
Struct.prototype.updateHalfBonds = function (list) {
function f(aid) {
this.atomUpdateHalfBonds(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.sGroupsRecalcCrossBonds = function () {
this.sgroups.each(function (sgid, sg) {
sg.xBonds = [];
sg.neiAtoms = [];
}, this);
this.bonds.each(function (bid, bond) {
var a1 = this.atoms.get(bond.begin);
var a2 = this.atoms.get(bond.end);
Set.each(a1.sgs, function (sgid) {
if (!Set.contains(a2.sgs, sgid)) {
var sg = this.sgroups.get(sgid);
sg.xBonds.push(bid);
arrayAddIfMissing(sg.neiAtoms, bond.end);
}
}, this);
Set.each(a2.sgs, function (sgid) {
if (!Set.contains(a1.sgs, sgid)) {
var sg = this.sgroups.get(sgid);
sg.xBonds.push(bid);
arrayAddIfMissing(sg.neiAtoms, bond.begin);
}
}, this);
}, this);
};
Struct.prototype.sGroupDelete = function (sgid) {
var sg = this.sgroups.get(sgid);
for (var i = 0; i < sg.atoms.length; ++i)
Set.remove(this.atoms.get(sg.atoms[i]).sgs, sgid);
this.sGroupForest.remove(sgid);
this.sgroups.remove(sgid);
};
Struct.prototype.atomSetPos = function (id, pp) {
var itemId = this['atoms'].get(id);
itemId.pp = pp;
};
Struct.prototype.rxnPlusSetPos = function (id, pp) {
var itemId = this['rxnPluses'].get(id);
itemId.pp = pp;
};
Struct.prototype.rxnArrowSetPos = function (id, pp) {
var itemId = this['rxnArrows'].get(id);
itemId.pp = pp;
};
Struct.prototype.getCoordBoundingBox = function (atomSet) {
var bb = null;
function extend(pp) {
if (!bb) {
bb = {
min: pp,
max: pp
};
} else {
bb.min = Vec2.min(bb.min, pp);
bb.max = Vec2.max(bb.max, pp);
}
}
var global = typeof (atomSet) === 'undefined';
this.atoms.each(function (aid, atom) {
if (global || Set.contains(atomSet, aid))
extend(atom.pp);
});
if (global) {
this.rxnPluses.each(function (id, item) {
extend(item.pp);
});
this.rxnArrows.each(function (id, item) {
extend(item.pp);
});
}
if (!bb && global) {
bb = {
min: new Vec2(0, 0),
max: new Vec2(1, 1)
};
}
return bb;
};
Struct.prototype.getCoordBoundingBoxObj = function () {
var bb = null;
function extend(pp) {
if (!bb) {
bb = {
min: new Vec2(pp),
max: new Vec2(pp)
};
} else {
bb.min = Vec2.min(bb.min, pp);
bb.max = Vec2.max(bb.max, pp);
}
}
this.atoms.each(function (aid, atom) {
extend(atom.pp);
});
return bb;
};
Struct.prototype.getBondLengthData = function () {
var totalLength = 0;
var cnt = 0;
this.bonds.each(function (bid, bond) {
totalLength += Vec2.dist(
this.atoms.get(bond.begin).pp,
this.atoms.get(bond.end).pp);
cnt++;
}, this);
return { cnt: cnt, totalLength: totalLength };
};
Struct.prototype.getAvgBondLength = function () {
var bld = this.getBondLengthData();
return bld.cnt > 0 ? bld.totalLength / bld.cnt : -1;
};
Struct.prototype.getAvgClosestAtomDistance = function () {
var totalDist = 0;
var minDist;
var dist = 0;
var keys = this.atoms.keys();
var k;
var j;
for (k = 0; k < keys.length; ++k) {
minDist = -1;
for (j = 0; j < keys.length; ++j) {
if (j == k)
continue; // eslint-disable-line no-continue
dist = Vec2.dist(this.atoms.get(keys[j]).pp, this.atoms.get(keys[k]).pp);
if (minDist < 0 || minDist > dist)
minDist = dist;
}
totalDist += minDist;
}
return keys.length > 0 ? totalDist / keys.length : -1;
};
Struct.prototype.checkBondExists = function (begin, end) {
var bondExists = false;
this.bonds.each(function (bid, bond) {
if ((bond.begin == begin && bond.end == end) ||
(bond.end == begin && bond.begin == end))
bondExists = true;
}, this);
return bondExists;
};
function Loop(/* Array of num*/hbs, /* Struct*/struct, /* bool*/convex) {
this.hbs = hbs; // set of half-bonds involved
this.dblBonds = 0; // number of double bonds in the loop
this.aromatic = true;
this.convex = convex || false;
hbs.forEach(function (hb) {
var bond = struct.bonds.get(struct.halfBonds.get(hb).bid);
if (bond.type != Bond.PATTERN.TYPE.AROMATIC)
this.aromatic = false;
if (bond.type == Bond.PATTERN.TYPE.DOUBLE)
this.dblBonds++;
}, this);
}
Struct.prototype.findConnectedComponent = function (aid) {
var map = {};
var list = [aid];
var ids = Set.empty();
while (list.length > 0) {
(function () {
var aid = list.pop();
map[aid] = 1;
Set.add(ids, aid);
var atom = this.atoms.get(aid);
for (var i = 0; i < atom.neighbors.length; ++i) {
var neiId = this.halfBonds.get(atom.neighbors[i]).end;
if (!Set.contains(ids, neiId))
list.push(neiId);
}
}).apply(this);
}
return ids;
};
Struct.prototype.findConnectedComponents = function (discardExistingFragments) {
// NB: this is a hack
// TODO: need to maintain half-bond and neighbor structure permanently
if (!this.halfBonds.count()) {
this.initHalfBonds();
this.initNeighbors();
this.updateHalfBonds(this.atoms.keys());
this.sortNeighbors(this.atoms.keys());
}
var map = {};
this.atoms.each(function (aid) {
map[aid] = -1;
}, this);
var components = [];
this.atoms.each(function (aid, atom) {
if ((discardExistingFragments || atom.fragment < 0) && map[aid] < 0) {
var component = this.findConnectedComponent(aid);
components.push(component);
Set.each(component, function (aid) {
map[aid] = 1;
}, this);
}
}, this);
return components;
};
Struct.prototype.markFragment = function (ids) {
var frag = {};
var fid = this.frags.add(frag);
Set.each(ids, function (aid) {
this.atoms.get(aid).fragment = fid;
}, this);
};
Struct.prototype.markFragmentByAtomId = function (aid) {
this.markFragment(this.findConnectedComponent(aid));
};
Struct.prototype.markFragments = function () {
var components = this.findConnectedComponents();
for (var i = 0; i < components.length; ++i)
this.markFragment(components[i]);
};
Struct.prototype.scale = function (scale) {
if (scale != 1) {
this.atoms.each(function (aid, atom) {
atom.pp = atom.pp.scaled(scale);
}, this);
this.rxnPluses.each(function (id, item) {
item.pp = item.pp.scaled(scale);
}, this);
this.rxnArrows.each(function (id, item) {
item.pp = item.pp.scaled(scale);
}, this);
this.sgroups.each(function (id, item) {
item.pp = item.pp ? item.pp.scaled(scale) : null;
}, this);
}
};
Struct.prototype.rescale = function () {
var avg = this.getAvgBondLength();
if (avg < 0 && !this.isReaction) // TODO [MK] this doesn't work well for reactions as the distances between
// the atoms in different components are generally larger than those between atoms of a single component
// (KETCHER-341)
avg = this.getAvgClosestAtomDistance();
if (avg < 1e-3)
avg = 1;
var scale = 1 / avg;
this.scale(scale);
};
Struct.prototype.loopHasSelfIntersections = function (hbs) {
for (var i = 0; i < hbs.length; ++i) {
var hbi = this.halfBonds.get(hbs[i]);
var ai = this.atoms.get(hbi.begin).pp;
var bi = this.atoms.get(hbi.end).pp;
var set = Set.fromList([hbi.begin, hbi.end]);
for (var j = i + 2; j < hbs.length; ++j) {
var hbj = this.halfBonds.get(hbs[j]);
if (Set.contains(set, hbj.begin) || Set.contains(set, hbj.end))
/* eslint-disable no-continue*/
continue; // skip edges sharing an atom
/* eslint-enable no-continue*/
var aj = this.atoms.get(hbj.begin).pp;
var bj = this.atoms.get(hbj.end).pp;
if (Vec2.segmentIntersection(ai, bi, aj, bj))
return true;
}
}
return false;
};
// partition a cycle into simple cycles
// TODO: [MK] rewrite the detection algorithm to only find simple ones right away?
Struct.prototype.partitionLoop = function (loop) { // eslint-disable-line max-statements
var subloops = [];
var continueFlag = true;
search: while (continueFlag) { // eslint-disable-line no-restricted-syntax
var atomToHalfBond = {}; // map from every atom in the loop to the index of the first half-bond starting from that atom in the uniqHb array
for (var l = 0; l < loop.length; ++l) {
var hbid = loop[l];
var aid1 = this.halfBonds.get(hbid).begin;
var aid2 = this.halfBonds.get(hbid).end;
if (aid2 in atomToHalfBond) { // subloop found
var s = atomToHalfBond[aid2]; // where the subloop begins
var subloop = loop.slice(s, l + 1);
subloops.push(subloop);
if (l < loop.length) // remove half-bonds corresponding to the subloop
loop.splice(s, l - s + 1);
continue search; // eslint-disable-line no-continue
}
atomToHalfBond[aid1] = l;
}
continueFlag = false; // we're done, no more subloops found
subloops.push(loop);
}
return subloops;
};
Struct.prototype.halfBondAngle = function (hbid1, hbid2) {
var hba = this.halfBonds.get(hbid1);
var hbb = this.halfBonds.get(hbid2);
return Math.atan2(
Vec2.cross(hba.dir, hbb.dir),
Vec2.dot(hba.dir, hbb.dir));
};
Struct.prototype.loopIsConvex = function (loop) {
for (var k = 0; k < loop.length; ++k) {
var angle = this.halfBondAngle(loop[k], loop[(k + 1) % loop.length]);
if (angle > 0)
return false;
}
return true;
};
// check whether a loop is on the inner or outer side of the polygon
// by measuring the total angle between bonds
Struct.prototype.loopIsInner = function (loop) {
var totalAngle = 2 * Math.PI;
for (var k = 0; k < loop.length; ++k) {
var hbida = loop[k];
var hbidb = loop[(k + 1) % loop.length];
var hbb = this.halfBonds.get(hbidb);
var angle = this.halfBondAngle(hbida, hbidb);
if (hbb.contra == loop[k]) // back and forth along the same edge
totalAngle += Math.PI;
else
totalAngle += angle;
}
return Math.abs(totalAngle) < Math.PI;
};
Struct.prototype.findLoops = function () {
var newLoops = [];
var bondsToMark = Set.empty();
/*
Starting from each half-bond not known to be in a loop yet,
follow the 'next' links until the initial half-bond is reached or
the length of the sequence exceeds the number of half-bonds available.
In a planar graph, as long as every bond is a part of some "loop" -
either an outer or an inner one - every iteration either yields a loop
or doesn't start at all. Thus this has linear complexity in the number
of bonds for planar graphs.
*/
var hbIdNext, c, loop, loopId;
this.halfBonds.each(function (hbId, hb) {
if (hb.loop !== -1)
return;
for (hbIdNext = hbId, c = 0, loop = []; c <= this.halfBonds.count(); hbIdNext = this.halfBonds.get(hbIdNext).next, ++c) {
if (!(c > 0 && hbIdNext === hbId)) {
loop.push(hbIdNext);
continue;
}
// loop found
var subloops = this.partitionLoop(loop);
subloops.forEach(function (loop) {
if (this.loopIsInner(loop) && !this.loopHasSelfIntersections(loop)) {
/*
loop is internal
use lowest half-bond id in the loop as the loop id
this ensures that the loop gets the same id if it is discarded and then recreated,
which in turn is required to enable redrawing while dragging, as actions store item id's
*/
loopId = Math.min.apply(Math, loop);
this.loops.set(loopId, new Loop(loop, this, this.loopIsConvex(loop)));
} else {
loopId = -2;
}
loop.forEach(function (hbid) {
this.halfBonds.get(hbid).loop = loopId;
Set.add(bondsToMark, this.halfBonds.get(hbid).bid);
}, this);
if (loopId >= 0)
newLoops.push(loopId);
}, this);
break;
}
}, this);
return {
newLoops: newLoops,
bondsToMark: Set.list(bondsToMark)
};
};
// NB: this updates the structure without modifying the corresponding ReStruct.
// To be applied to standalone structures only.
Struct.prototype.prepareLoopStructure = function () {
this.initHalfBonds();
this.initNeighbors();
this.updateHalfBonds(this.atoms.keys());
this.sortNeighbors(this.atoms.keys());
this.findLoops();
};
Struct.prototype.atomAddToSGroup = function (sgid, aid) {
// TODO: [MK] make sure the addition does not break the hierarchy?
SGroup.addAtom(this.sgroups.get(sgid), aid);
Set.add(this.atoms.get(aid).sgs, sgid);
};
Struct.prototype.calcConn = function (aid) {
var conn = 0;
var atom = this.atoms.get(aid);
var oddLoop = false;
var hasAromatic = false;
for (var i = 0; i < atom.neighbors.length; ++i) {
var hb = this.halfBonds.get(atom.neighbors[i]);
var bond = this.bonds.get(hb.bid);
switch (bond.type) {
case Bond.PATTERN.TYPE.SINGLE:
conn += 1;
break;
case Bond.PATTERN.TYPE.DOUBLE:
conn += 2;
break;
case Bond.PATTERN.TYPE.TRIPLE:
conn += 3;
break;
case Bond.PATTERN.TYPE.AROMATIC:
conn += 1;
hasAromatic = true;
this.loops.each(function (id, item) {
if (item.hbs.indexOf(atom.neighbors[i]) != -1 && item.hbs.length % 2 == 1)
oddLoop = true;
}, this);
break;
default:
return -1;
}
}
if (hasAromatic && !atom.hasImplicitH && !oddLoop)
conn += 1;
return conn;
};
Struct.prototype.calcImplicitHydrogen = function (aid) {
var conn = this.calcConn(aid);
var atom = this.atoms.get(aid);
atom.badConn = false;
if (conn < 0 || atom.isQuery()) {
atom.implicitH = 0;
return;
}
if (atom.explicitValence >= 0) {
var elem = element.map[atom.label];
atom.implicitH = 0;
if (elem != null) {
atom.implicitH = atom.explicitValence - atom.calcValenceMinusHyd(conn);
if (atom.implicitH < 0) {
atom.implicitH = 0;
atom.badConn = true;
}
}
} else {
atom.calcValence(conn);
}
};
Struct.prototype.setImplicitHydrogen = function (list) {
this.sgroups.each(function (id, item) {
if (item.data.fieldName === "MRV_IMPLICIT_H")
this.atoms.get(item.atoms[0]).hasImplicitH = true;
}, this);
function f(aid) {
this.calcImplicitHydrogen(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.getComponents = function () { // eslint-disable-line max-statements
/* saver */
var ccs = this.findConnectedComponents(true);
var barriers = [];
var arrowPos = null;
this.rxnArrows.each(function (id, item) { // there's just one arrow
arrowPos = item.pp.x;
});
this.rxnPluses.each(function (id, item) {
barriers.push(item.pp.x);
});
if (arrowPos != null)
barriers.push(arrowPos);
barriers.sort(function (a, b) {
return a - b;
});
var components = [];
var i;
for (i = 0; i < ccs.length; ++i) {
var bb = this.getCoordBoundingBox(ccs[i]);
var c = Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
var j = 0;
while (c.x > barriers[j])
++j;
components[j] = components[j] || {};
Set.mergeIn(components[j], ccs[i]);
}
var submolTexts = [];
var reactants = [];
var products = [];
for (i = 0; i < components.length; ++i) {
if (!components[i]) {
submolTexts.push('');
continue; // eslint-disable-line no-continue
}
bb = this.getCoordBoundingBox(components[i]);
c = Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
if (c.x < arrowPos)
reactants.push(components[i]);
else
products.push(components[i]);
}
return {
reactants: reactants,
products: products
};
};
// Other struct objects
function RxnPlus(params) {
params = params || {};
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
}
RxnPlus.prototype.clone = function () {
return new RxnPlus(this);
};
function RxnArrow(params) {
params = params || {};
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
}
RxnArrow.prototype.clone = function () {
return new RxnArrow(this);
};
function arrayAddIfMissing(array, item) {
for (var i = 0; i < array.length; ++i) {
if (array[i] === item)
return false;
}
array.push(item);
return true;
}
module.exports = Object.assign(Struct, {
Atom: Atom,
AtomList: AtomList,
Bond: Bond,
SGroup: SGroup,
RGroup: RGroup,
RxnPlus: RxnPlus,
RxnArrow: RxnArrow
});

View File

@ -0,0 +1,51 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Pool = require('../../util/pool');
function RGroup(logic) {
logic = logic || {};
this.frags = new Pool();
this.resth = logic.resth || false;
this.range = logic.range || '';
this.ifthen = logic.ifthen || 0;
}
RGroup.prototype.getAttrs = function () {
return {
resth: this.resth,
range: this.range,
ifthen: this.ifthen
};
};
RGroup.findRGroupByFragment = function (rgroups, frid) {
var ret;
rgroups.each(function (rgid, rgroup) {
if (rgroup.frags.keyOf(frid)) ret = rgid;
});
return ret;
};
RGroup.prototype.clone = function (fidMap) {
var ret = new RGroup(this);
this.frags.each(function (fnum, fid) {
ret.frags.add(fidMap ? fidMap[fid] : fid);
});
return ret;
};
module.exports = RGroup;

View File

@ -0,0 +1,164 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Map = require('../../util/map');
var Set = require('../../util/set');
function SGroupForest(molecule) {
this.parent = new Map(); // child id -> parent id
this.children = new Map(); // parent id -> list of child ids
this.children.set(-1, []); // extra root node
this.molecule = molecule;
}
// returns an array or s-group ids in the order of breadth-first search
SGroupForest.prototype.getSGroupsBFS = function () {
var order = [];
var id = -1;
var queue = [].slice.call(this.children.get(-1));
while (queue.length > 0) {
id = queue.shift();
queue = queue.concat(this.children.get(id));
order.push(id);
}
return order;
};
SGroupForest.prototype.getAtomSets = function () {
return this.molecule.sgroups.map(function (sgid, sgroup) {
return Set.fromList(sgroup.atoms);
});
};
SGroupForest.prototype.getAtomSetRelations = function (newId, atoms /* Set */) {
// find the lowest superset in the hierarchy
var isStrictSuperset = new Map();
var isSubset = new Map();
var atomSets = this.getAtomSets();
atomSets.unset(newId);
atomSets.each(function (id, atomSet) {
isSubset.set(id, Set.subset(atoms, atomSet));
isStrictSuperset.set(id, Set.subset(atomSet, atoms) && !Set.eq(atomSet, atoms));
}, this);
var parents = atomSets.findAll(function (id) {
if (!isSubset.get(id))
return false;
if (this.children.get(id).findIndex(function (childId) {
return isSubset.get(childId);
}, this) >= 0)
return false;
return true;
}, this);
console.assert(parents.length <= 1, "We are here"); // there should be only one parent
var children = atomSets.findAll(function (id) {
return isStrictSuperset.get(id) && !isStrictSuperset.get(this.parent.get(id));
}, this);
return {
children: children,
parent: parents.length === 0 ? -1 : parents[0]
};
};
SGroupForest.prototype.getPathToRoot = function (sgid) {
var path = [];
for (var id = sgid; id >= 0; id = this.parent.get(id)) {
console.assert(path.indexOf(id) < 0, 'SGroupForest: loop detected');
path.push(id);
}
return path;
};
SGroupForest.prototype.validate = function () {
var atomSets = this.getAtomSets();
this.molecule.sgroups.each(function (id) {
this.getPathToRoot(id); // this will throw an exception if there is a loop in the path to root
}, this);
var valid = true;
// 1) child group's atom set is a subset of the parent one's
this.parent.each(function (id, parentId) {
if (parentId >= 0 && !Set.subset(atomSets.get(id), atomSets.get(parentId)))
valid = false;
}, this);
// 2) siblings have disjoint atom sets
this.children.each(function (parentId) {
var list = this.children.get(parentId);
for (var i = 0; i < list.length; ++i) {
for (var j = i + 1; j < list.length; ++j) {
var id1 = list[i];
var id2 = list[j];
var sg1 = this.molecule.sgroups.get(id1);
var sg2 = this.molecule.sgroups.get(id2);
if (!Set.disjoint(atomSets.get(id1), atomSets.get(id2)) && sg1.type != 'DAT' && sg2.type != 'DAT')
valid = false;
}
}
}, this);
return valid;
};
SGroupForest.prototype.insert = function (id, parent /* int, optional */, children /* [int], optional */) {
console.assert(!this.parent.has(id), 'sgid already present in the forest');
console.assert(!this.children.has(id), 'sgid already present in the forest');
console.assert(this.validate(), 's-group forest invalid');
var atomSets = this.getAtomSets();
var atoms = Set.fromList(this.molecule.sgroups.get(id).atoms);
if (!parent || !children) { // if these are not provided, deduce automatically
var guess = this.getAtomSetRelations(id, atoms, atomSets);
parent = guess.parent;
children = guess.children;
}
// TODO: make children Map<int, Set> instead of Map<int, []>?
children.forEach(function (childId) { // reset parent links
var childs = this.children.get(this.parent.get(childId));
var i = childs.indexOf(childId);
console.assert(i >= 0 && childs.indexOf(childId, i + 1) < 0, 'Assertion failed'); // one element
childs.splice(i, 1);
this.parent.set(childId, id);
}, this);
this.children.set(id, children);
this.parent.set(id, parent);
this.children.get(parent).push(id);
console.assert(this.validate(), 's-group forest invalid');
return { parent: parent, children: children };
};
SGroupForest.prototype.remove = function (id) {
console.assert(this.parent.has(id), 'sgid is not in the forest');
console.assert(this.children.has(id), 'sgid is not in the forest');
console.assert(this.validate(), 's-group forest invalid');
var parentId = this.parent.get(id);
this.children.get(id).forEach(function (childId) { // reset parent links
this.parent.set(childId, parentId);
this.children.get(parentId).push(childId);
}, this);
var childs = this.children.get(parentId);
var i = childs.indexOf(id);
console.assert(i >= 0 && childs.indexOf(id, i + 1) < 0, 'Assertion failed'); // one element
childs.splice(i, 1);
this.children.unset(id);
this.parent.unset(id);
console.assert(this.validate(), 's-group forest invalid');
};
module.exports = SGroupForest;

View File

@ -0,0 +1,383 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Atom = require('./atom');
var Bond = require('./bond');
function SGroup(type) { // eslint-disable-line max-statements
console.assert(type && type in SGroup.TYPES, 'Invalid or unsupported s-group type');
this.type = type;
this.id = -1;
this.label = -1;
this.bracketBox = null;
this.bracketDir = new Vec2(1, 0);
this.areas = [];
this.highlight = false;
this.highlighting = null;
this.selected = false;
this.selectionPlate = null;
this.atoms = [];
this.patoms = [];
this.bonds = [];
this.xBonds = [];
this.neiAtoms = [];
this.pp = null;
this.data = {
mul: 1, // multiplication count for MUL group
connectivity: 'ht', // head-to-head, head-to-tail or either-unknown
name: '',
subscript: 'n',
// data s-group fields
attached: false,
absolute: true,
showUnits: false,
nCharsToDisplay: -1,
tagChar: '',
daspPos: 1,
fieldType: 'F',
fieldName: '',
fieldValue: '',
units: '',
query: '',
queryOp: ''
};
}
SGroup.TYPES = {
MUL: 1,
SRU: 2,
SUP: 3,
DAT: 4,
GEN: 5
};
// TODO: these methods should be overridden
// and should only accept valid attributes for each S-group type.
// The attributes should be accessed via these methods only and not directly through this.data.
// stub
SGroup.prototype.getAttr = function (attr) {
return this.data[attr];
};
// TODO: should be group-specific
SGroup.prototype.getAttrs = function () {
var attrs = {};
for (var attr in this.data) {
if (this.data.hasOwnProperty(attr))
attrs[attr] = this.data[attr];
}
return attrs;
};
// stub
SGroup.prototype.setAttr = function (attr, value) {
var oldValue = this.data[attr];
this.data[attr] = value;
return oldValue;
};
// stub
SGroup.prototype.checkAttr = function (attr, value) {
return this.data[attr] == value;
};
// SGroup.numberArrayToString = function (numbers, map) {
// var str = util.stringPadded(numbers.length, 3);
// for (var i = 0; i < numbers.length; ++i) {
// str += ' ' + util.stringPadded(map[numbers[i]], 3);
// }
// return str;
// };
SGroup.filterAtoms = function (atoms, map) {
var newAtoms = [];
for (var i = 0; i < atoms.length; ++i) {
var aid = atoms[i];
if (typeof (map[aid]) !== 'number')
newAtoms.push(aid);
else if (map[aid] >= 0)
newAtoms.push(map[aid]);
else
newAtoms.push(-1);
}
return newAtoms;
};
SGroup.removeNegative = function (atoms) {
var newAtoms = [];
for (var j = 0; j < atoms.length; ++j) {
if (atoms[j] >= 0)
newAtoms.push(atoms[j]);
}
return newAtoms;
};
SGroup.filter = function (mol, sg, atomMap) {
sg.atoms = SGroup.removeNegative(SGroup.filterAtoms(sg.atoms, atomMap));
};
SGroup.clone = function (sgroup, aidMap) {
var cp = new SGroup(sgroup.type);
for (var field in sgroup.data) // TODO: remove all non-primitive properties from 'data'
cp.data[field] = sgroup.data[field];
cp.atoms = sgroup.atoms.map(function (elem) {
return aidMap[elem];
});
cp.pp = sgroup.pp;
cp.bracketBox = sgroup.bracketBox;
cp.patoms = null;
cp.bonds = null;
cp.allAtoms = sgroup.allAtoms;
return cp;
};
SGroup.addAtom = function (sgroup, aid) {
sgroup.atoms.push(aid);
};
SGroup.removeAtom = function (sgroup, aid) {
for (var i = 0; i < sgroup.atoms.length; ++i) {
if (sgroup.atoms[i] === aid) {
sgroup.atoms.splice(i, 1);
return;
}
}
console.error('The atom is not found in the given s-group');
};
SGroup.getCrossBonds = function (inBonds, xBonds, mol, parentAtomSet) {
mol.bonds.each(function (bid, bond) {
if (Set.contains(parentAtomSet, bond.begin) && Set.contains(parentAtomSet, bond.end)) {
if (inBonds != null)
inBonds.push(bid);
} else if (Set.contains(parentAtomSet, bond.begin) || Set.contains(parentAtomSet, bond.end)) {
if (xBonds != null)
xBonds.push(bid);
}
}, this);
};
SGroup.bracketPos = function (sg, mol, xbonds) { // eslint-disable-line max-statements
var atoms = sg.atoms;
if (!xbonds || xbonds.length !== 2) {
sg.bracketDir = new Vec2(1, 0);
} else {
var p1 = mol.bonds.get(xbonds[0]).getCenter(mol);
var p2 = mol.bonds.get(xbonds[1]).getCenter(mol);
sg.bracketDir = Vec2.diff(p2, p1).normalized();
}
var d = sg.bracketDir;
var bb = null;
var contentBoxes = [];
atoms.forEach(function (aid) {
var atom = mol.atoms.get(aid);
var pos = new Vec2(atom.pp);
var ext = new Vec2(0.05 * 3, 0.05 * 3);
var bba = new Box2Abs(pos, pos).extend(ext, ext);
contentBoxes.push(bba);
}, this);
contentBoxes.forEach(function (bba) {
var bbb = null;
[bba.p0.x, bba.p1.x].forEach(function (x) {
[bba.p0.y, bba.p1.y].forEach(function (y) {
var v = new Vec2(x, y);
var p = new Vec2(Vec2.dot(v, d), Vec2.dot(v, d.rotateSC(1, 0)));
bbb = (bbb === null) ? new Box2Abs(p, p) : bbb.include(p);
}, this);
}, this);
bb = (bb === null) ? bbb : Box2Abs.union(bb, bbb);
}, this);
var vext = new Vec2(0.2, 0.4);
if (bb !== null) bb = bb.extend(vext, vext);
sg.bracketBox = bb;
};
SGroup.getBracketParameters = function (mol, xbonds, atomSet, bb, d, n) { // eslint-disable-line max-params
function BracketParams(c, d, w, h) {
this.c = c;
this.d = d;
this.n = d.rotateSC(1, 0);
this.w = w;
this.h = h;
}
var brackets = [];
if (xbonds.length < 2) {
(function () {
d = d || new Vec2(1, 0);
n = n || d.rotateSC(1, 0);
var bracketWidth = Math.min(0.25, bb.sz().x * 0.3);
var cl = Vec2.lc2(d, bb.p0.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var cr = Vec2.lc2(d, bb.p1.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var bracketHeight = bb.sz().y;
brackets.push(new BracketParams(cl, d.negated(), bracketWidth, bracketHeight), new BracketParams(cr, d, bracketWidth, bracketHeight));
})();
} else if (xbonds.length === 2) {
(function () { // eslint-disable-line max-statements
var b1 = mol.bonds.get(xbonds[0]);
var b2 = mol.bonds.get(xbonds[1]);
var cl0 = b1.getCenter(mol);
var cr0 = b2.getCenter(mol);
var dr = Vec2.diff(cr0, cl0).normalized();
var dl = dr.negated();
var bracketWidth = 0.25;
var bracketHeight = 1.5;
brackets.push(new BracketParams(cl0.addScaled(dl, 0), dl, bracketWidth, bracketHeight),
new BracketParams(cr0.addScaled(dr, 0), dr, bracketWidth, bracketHeight));
})();
} else {
(function () {
for (var i = 0; i < xbonds.length; ++i) {
var b = mol.bonds.get(xbonds[i]);
var c = b.getCenter(mol);
var d = Set.contains(atomSet, b.begin) ? b.getDir(mol) : b.getDir(mol).negated();
brackets.push(new BracketParams(c, d, 0.2, 1.0));
}
})();
}
return brackets;
};
SGroup.getObjBBox = function (atoms, mol) {
console.assert(atoms.length != 0, 'Atom list is empty');
var a0 = mol.atoms.get(atoms[0]).pp;
var bb = new Box2Abs(a0, a0);
for (var i = 1; i < atoms.length; ++i) {
var aid = atoms[i];
var atom = mol.atoms.get(aid);
var p = atom.pp;
bb = bb.include(p);
}
return bb;
};
SGroup.getAtoms = function (mol, sg) {
/* shoud we use prototype? */
if (!sg.allAtoms)
return sg.atoms;
var atoms = [];
mol.atoms.each(function (aid) {
atoms.push(aid);
});
return atoms;
};
SGroup.getBonds = function (mol, sg) {
var atoms = SGroup.getAtoms(mol, sg);
var bonds = [];
mol.bonds.each(function (bid, bond) {
if (atoms.indexOf(bond.begin) >= 0 && atoms.indexOf(bond.end) >= 0) bonds.push(bid);
});
return bonds;
};
SGroup.prepareMulForSaving = function (sgroup, mol) { // eslint-disable-line max-statements
sgroup.atoms.sort((a, b) => a - b);
sgroup.atomSet = Set.fromList(sgroup.atoms);
sgroup.parentAtomSet = Set.clone(sgroup.atomSet);
var inBonds = [];
var xBonds = [];
mol.bonds.each(function (bid, bond) {
if (Set.contains(sgroup.parentAtomSet, bond.begin) && Set.contains(sgroup.parentAtomSet, bond.end))
inBonds.push(bid);
else if (Set.contains(sgroup.parentAtomSet, bond.begin) || Set.contains(sgroup.parentAtomSet, bond.end))
xBonds.push(bid);
}, sgroup);
if (xBonds.length !== 0 && xBonds.length !== 2) {
throw {
'id': sgroup.id,
'error-type': 'cross-bond-number',
'message': 'Unsupported cross-bonds number'
};
}
var xAtom1 = -1;
var xAtom2 = -1;
var crossBond = null;
if (xBonds.length === 2) {
var bond1 = mol.bonds.get(xBonds[0]);
xAtom1 = Set.contains(sgroup.parentAtomSet, bond1.begin) ? bond1.begin : bond1.end;
var bond2 = mol.bonds.get(xBonds[1]);
xAtom2 = Set.contains(sgroup.parentAtomSet, bond2.begin) ? bond2.begin : bond2.end;
crossBond = bond2;
}
var amap = null;
var tailAtom = xAtom2;
var newAtoms = [];
for (var j = 0; j < sgroup.data.mul - 1; j++) {
amap = {};
sgroup.atoms.forEach(function (aid) {
var atom = mol.atoms.get(aid);
var aid2 = mol.atoms.add(new Atom(atom));
newAtoms.push(aid2);
sgroup.atomSet[aid2] = 1;
amap[aid] = aid2;
});
inBonds.forEach(function (bid) {
var bond = mol.bonds.get(bid);
var newBond = new Bond(bond);
newBond.begin = amap[newBond.begin];
newBond.end = amap[newBond.end];
mol.bonds.add(newBond);
});
if (crossBond !== null) {
var newCrossBond = new Bond(crossBond);
newCrossBond.begin = tailAtom;
newCrossBond.end = amap[xAtom1];
mol.bonds.add(newCrossBond);
tailAtom = amap[xAtom2];
}
}
if (tailAtom >= 0) {
var xBond2 = mol.bonds.get(xBonds[1]);
if (xBond2.begin === xAtom2)
xBond2.begin = tailAtom;
else
xBond2.end = tailAtom;
}
sgroup.bonds = xBonds;
newAtoms.forEach(function (aid) {
mol.sGroupForest.getPathToRoot(sgroup.id).reverse().forEach(function (sgid) {
mol.atomAddToSGroup(sgid, aid);
});
});
};
SGroup.getMassCentre = function (mol, atoms) {
var c = new Vec2(); // mass centre
for (var i = 0; i < atoms.length; ++i)
c = c.addScaled(mol.atoms.get(atoms[i]).pp, 1.0 / atoms.length);
return c;
};
module.exports = SGroup;