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

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;