Files
enviPy-bayer/static/js/ketcher2/script/chem/struct/index.js
2025-06-23 20:13:54 +02:00

976 lines
26 KiB
JavaScript

/****************************************************************************
* 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
});