forked from enviPath/enviPy
976 lines
26 KiB
JavaScript
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
|
|
});
|