smiles-drawer
Version:
A SMILES drawer and parser. Generate molecular structure depictions in pure JavaScript.
1,343 lines (1,148 loc) • 139 kB
JavaScript
// @ts-check
import ArrayHelper from './ArrayHelper';
import Atom from './Atom';
import CanvasWrapper from './CanvasWrapper';
import CIP from './CIP';
import Edge from './Edge';
import Graph from './Graph';
import Line from './Line';
import MathHelper from './MathHelper';
import Options from './Options';
import Ring from './Ring';
import RingConnection from './RingConnection';
import SSSR from './SSSR';
import ThemeManager from './ThemeManager';
import Vector2 from './Vector2';
import Vertex from './Vertex';
/**
* The main class of the application representing the smiles drawer
*
* @property {Graph} graph The graph associated with this SmilesDrawer.Drawer instance.
* @property {Number} ringIdCounter An internal counter to keep track of ring ids.
* @property {Number} ringConnectionIdCounter An internal counter to keep track of ring connection ids.
* @property {CanvasWrapper} canvasWrapper The CanvasWrapper associated with this SmilesDrawer.Drawer instance.
* @property {Number} totalOverlapScore The current internal total overlap score.
* @property {Object} defaultOptions The default options.
* @property {Object} opts The merged options.
* @property {Object} theme The current theme.
*/
export default class DrawerBase {
/**
* The constructor for the class SmilesDrawer.
*
* @param {Object} options An object containing custom values for different options. It is merged with the default options.
*/
constructor(options) {
this.graph = null;
this.doubleBondConfigCount = 0;
this.doubleBondConfig = null;
this.ringIdCounter = 0;
this.ringConnectionIdCounter = 0;
this.canvasWrapper = null;
this.totalOverlapScore = 0;
this.defaultOptions = {
width: 500,
height: 500,
scale: 0.0,
bondThickness: 1.0,
bondLength: 30,
shortBondLength: 0.8,
bondSpacing: 0.17 * 30,
atomVisualization: 'default',
isomeric: true,
debug: false,
terminalCarbons: false,
showCarbons: 'default',
explicitHydrogens: true,
overlapSensitivity: 0.42,
overlapResolutionIterations: 1,
compactDrawing: true,
fontFamily: 'Arial, Helvetica, sans-serif',
fontSizeLarge: 11,
fontSizeSmall: 3,
padding: 10.0,
experimentalSSSR: false,
experimentalWeights: false,
kkThreshold: 0.1,
kkInnerThreshold: 0.1,
kkMaxIteration: 20000,
kkMaxInnerIteration: 50,
kkMaxEnergy: 1e9,
weights: {
colormap: null,
additionalPadding: 20.0,
sigma: 10,
interval: 0.0,
opacity: 1.0,
},
themes: {
'dark': {
FOREGROUND: '#ffffff',
BACKGROUND: '#141414',
C: '#ffffff',
O: '#e74c3c',
N: '#3498db',
F: '#27ae60',
CL: '#16a085',
BR: '#d35400',
I: '#8e44ad',
P: '#d35400',
S: '#f1c40f',
B: '#e67e22',
SI: '#e67e22',
H: '#aaaaaa',
},
'light': {
FOREGROUND: '#222222',
BACKGROUND: '#ffffff',
C: '#222222',
O: '#e74c3c',
N: '#3498db',
F: '#27ae60',
CL: '#16a085',
BR: '#d35400',
I: '#8e44ad',
P: '#d35400',
S: '#f1c40f',
B: '#e67e22',
SI: '#e67e22',
H: '#666666',
},
'oldschool': {
FOREGROUND: '#000000',
BACKGROUND: '#ffffff',
C: '#000000',
O: '#000000',
N: '#000000',
F: '#000000',
CL: '#000000',
BR: '#000000',
I: '#000000',
P: '#000000',
S: '#000000',
B: '#000000',
SI: '#000000',
H: '#000000',
},
'solarized': {
FOREGROUND: '#586e75',
BACKGROUND: '#eee8d5',
C: '#586e75',
O: '#dc322f',
N: '#268bd2',
F: '#859900',
CL: '#16a085',
BR: '#cb4b16',
I: '#6c71c4',
P: '#d33682',
S: '#b58900',
B: '#2aa198',
SI: '#2aa198',
H: '#657b83',
},
'solarized-dark': {
FOREGROUND: '#93a1a1',
BACKGROUND: '#073642',
C: '#93a1a1',
O: '#dc322f',
N: '#268bd2',
F: '#859900',
CL: '#16a085',
BR: '#cb4b16',
I: '#6c71c4',
P: '#d33682',
S: '#b58900',
B: '#2aa198',
SI: '#2aa198',
H: '#839496',
},
'matrix': {
FOREGROUND: '#678c61',
BACKGROUND: '#ffffff',
C: '#678c61',
O: '#2fc079',
N: '#4f7e7e',
F: '#90d762',
CL: '#82d967',
BR: '#23755a',
I: '#409931',
P: '#c1ff8a',
S: '#faff00',
B: '#50b45a',
SI: '#409931',
H: '#426644',
},
'github': {
FOREGROUND: '#24292f',
BACKGROUND: '#ffffff',
C: '#24292f',
O: '#cf222e',
N: '#0969da',
F: '#2da44e',
CL: '#6fdd8b',
BR: '#bc4c00',
I: '#8250df',
P: '#bf3989',
S: '#d4a72c',
B: '#fb8f44',
SI: '#bc4c00',
H: '#57606a',
},
'carbon': {
FOREGROUND: '#161616',
BACKGROUND: '#ffffff',
C: '#161616',
O: '#da1e28',
N: '#0f62fe',
F: '#198038',
CL: '#007d79',
BR: '#fa4d56',
I: '#8a3ffc',
P: '#ff832b',
S: '#f1c21b',
B: '#8a3800',
SI: '#e67e22',
H: '#525252',
},
'cyberpunk': {
FOREGROUND: '#ea00d9',
BACKGROUND: '#ffffff',
C: '#ea00d9',
O: '#ff3131',
N: '#0abdc6',
F: '#00ff9f',
CL: '#00fe00',
BR: '#fe9f20',
I: '#ff00ff',
P: '#fe7f00',
S: '#fcee0c',
B: '#ff00ff',
SI: '#ffffff',
H: '#913cb1',
},
'gruvbox': {
FOREGROUND: '#665c54',
BACKGROUND: '#fbf1c7',
C: '#665c54',
O: '#cc241d',
N: '#458588',
F: '#98971a',
CL: '#79740e',
BR: '#d65d0e',
I: '#b16286',
P: '#af3a03',
S: '#d79921',
B: '#689d6a',
SI: '#427b58',
H: '#7c6f64',
},
'gruvbox-dark': {
FOREGROUND: '#ebdbb2',
BACKGROUND: '#282828',
C: '#ebdbb2',
O: '#cc241d',
N: '#458588',
F: '#98971a',
CL: '#b8bb26',
BR: '#d65d0e',
I: '#b16286',
P: '#fe8019',
S: '#d79921',
B: '#8ec07c',
SI: '#83a598',
H: '#bdae93',
},
'custom': {
FOREGROUND: '#222222',
BACKGROUND: '#ffffff',
C: '#222222',
O: '#e74c3c',
N: '#3498db',
F: '#27ae60',
CL: '#16a085',
BR: '#d35400',
I: '#8e44ad',
P: '#d35400',
S: '#f1c40f',
B: '#e67e22',
SI: '#e67e22',
H: '#666666',
},
},
};
this.opts = Options.extend(true, this.defaultOptions, options);
const allowedShowCarbons = ['none', 'default', 'terminal', 'acyclic', 'all'];
if (allowedShowCarbons.indexOf(this.opts.showCarbons) === -1) {
this.opts.showCarbons = 'default';
}
this.opts.halfBondSpacing = this.opts.bondSpacing / 2.0;
this.opts.bondLengthSq = this.opts.bondLength * this.opts.bondLength;
this.opts.halfFontSizeLarge = this.opts.fontSizeLarge / 2.0;
this.opts.quarterFontSizeLarge = this.opts.fontSizeLarge / 4.0;
this.opts.fifthFontSizeSmall = this.opts.fontSizeSmall / 5.0;
// Set the default theme.
this.theme = this.opts.themes.dark;
}
/**
* Resolves carbon label display mode, including legacy `terminalCarbons` when `showCarbons` is `'default'`.
*
* @param {Object} opts Merged drawer options.
* @returns {'none'|'default'|'terminal'|'acyclic'|'all'}
*/
static getEffectiveShowCarbonsMode(opts) {
const allowed = ['none', 'default', 'terminal', 'acyclic', 'all'];
let mode = opts.showCarbons;
if (mode === undefined || mode === null || allowed.indexOf(mode) === -1) {
mode = 'default';
}
if (mode === 'default' && opts.terminalCarbons) {
return 'terminal';
}
return mode;
}
/**
* Draws the parsed smiles data to a canvas element.
*
* @param {Object} data The tree returned by the smiles parser.
* @param {(String|HTMLCanvasElement)} target The id of the HTML canvas element the structure is drawn to - or the element itself.
* @param {String} themeName='dark' The name of the theme to use. Built-in themes are 'light' and 'dark'.
* @param {Boolean} infoOnly=false Only output info on the molecule without drawing anything to the canvas.
*/
draw(data, target, themeName = 'light', infoOnly = false) {
this.initDraw(data, themeName, infoOnly);
if (!this.infoOnly) {
this.themeManager = new ThemeManager(this.opts.themes, themeName);
this.canvasWrapper = new CanvasWrapper(target, this.themeManager, this.opts);
}
if (!infoOnly) {
this.processGraph();
// Set the canvas to the appropriate size
this.canvasWrapper.scale(this.graph.vertices);
// Do the actual drawing
this.drawEdges(this.opts.debug);
this.drawVertices(this.opts.debug);
this.canvasWrapper.reset();
if (this.opts.debug) {
console.debug('DrawerBase::draw()', {
graph: this.graph,
rings: this.rings,
ringConnections: this.ringConnections,
});
}
}
}
/**
* Returns the number of rings this edge is a part of.
*
* @param {Number} edgeId The id of an edge.
* @returns {Number} The number of rings the provided edge is part of.
*/
edgeRingCount(edgeId) {
let edge = this.graph.edges[edgeId];
let a = this.graph.vertices[edge.sourceId];
let b = this.graph.vertices[edge.targetId];
return Math.min(a.value.rings.length, b.value.rings.length);
}
/**
* Returns an array containing the bridged rings associated with this molecule.
*
* @returns {Ring[]} An array containing all bridged rings associated with this molecule.
*/
getBridgedRings() {
return this.rings.filter(ring => ring.isBridged);
}
/**
* Returns an array containing all fused rings associated with this molecule.
*
* @returns {Ring[]} An array containing all fused rings associated with this molecule.
*/
getFusedRings() {
return this.rings.filter(ring => ring.isFused);
}
/**
* Returns an array containing all spiros associated with this molecule.
*
* @returns {Ring[]} An array containing all spiros associated with this molecule.
*/
getSpiros() {
return this.rings.filter(ring => ring.isSpiro);
}
/**
* Returns a string containing a semicolon and new-line separated list of ring properties: Id; Members Count; Neighbours Count; IsSpiro; IsFused; IsBridged; Ring Count (subrings of bridged rings)
*
* @returns {String} A string as described in the method description.
*/
printRingInfo() {
let result = '';
for (let i = 0; i < this.rings.length; i++) {
const ring = this.rings[i];
result += ring.id + ';';
result += ring.members.length + ';';
result += ring.neighbours.length + ';';
result += ring.isSpiro ? 'true;' : 'false;';
result += ring.isFused ? 'true;' : 'false;';
result += ring.isBridged ? 'true;' : 'false;';
result += ring.rings.length + ';';
result += '\n';
}
return result;
}
/**
* Rotates the drawing to make the widest dimension horizontal.
*/
rotateDrawing() {
// Rotate the vertices to make the molecule align horizontally
// Find the longest distance
let a = 0;
let b = 0;
let maxDist = 0;
for (let i = 0; i < this.graph.vertices.length; i++) {
let vertexA = this.graph.vertices[i];
if (!vertexA.value.isDrawn) {
continue;
}
for (let j = i + 1; j < this.graph.vertices.length; j++) {
let vertexB = this.graph.vertices[j];
if (!vertexB.value.isDrawn) {
continue;
}
let dist = vertexA.position.distanceSq(vertexB.position);
if (dist > maxDist) {
maxDist = dist;
a = i;
b = j;
}
}
}
let angle = -Vector2.subtract(this.graph.vertices[a].position, this.graph.vertices[b].position).angle();
if (!isNaN(angle)) {
// Round to 30 degrees
let remainder = angle % 0.523599;
// Round either up or down in 30 degree steps
if (remainder < 0.2617995) {
angle = angle - remainder;
}
else {
angle += 0.523599 - remainder;
}
// Finally, rotate everything
for (let i = 0; i < this.graph.vertices.length; i++) {
if (i === b) {
continue;
}
this.graph.vertices[i].position.rotateAround(angle, this.graph.vertices[b].position);
}
for (let i = 0; i < this.rings.length; i++) {
this.rings[i].center.rotateAround(angle, this.graph.vertices[b].position);
}
}
}
/**
* Returns the total overlap score of the current molecule.
*
* @returns {Number} The overlap score.
*/
getTotalOverlapScore() {
return this.totalOverlapScore;
}
/**
* Returns the ring count of the current molecule.
*
* @returns {Number} The ring count.
*/
getRingCount() {
return this.rings.length;
}
/**
* Checks whether or not the current molecule a bridged ring.
*
* @returns {Boolean} A boolean indicating whether or not the current molecule a bridged ring.
*/
hasBridgedRing() {
return this.bridgedRing;
}
/**
* Returns the number of heavy atoms (non-hydrogen) in the current molecule.
*
* @returns {Number} The heavy atom count.
*/
getHeavyAtomCount() {
let hac = 0;
for (let i = 0; i < this.graph.vertices.length; i++) {
if (this.graph.vertices[i].value.element !== 'H') {
hac++;
}
}
return hac;
}
/**
* Returns the molecular formula of the loaded molecule as a string.
*
* @returns {String} The molecular formula.
*/
getMolecularFormula(data = null) {
let molecularFormula = '';
let counts = new Map();
let graph = data === null ? this.graph : new Graph(data, this.opts.isomeric);
// Initialize element count
for (let i = 0; i < graph.vertices.length; i++) {
let atom = graph.vertices[i].value;
const a = counts.get(atom.element) || 0;
counts.set(atom.element, a + 1);
const hydrogens = atom.countImplicitHydrogens();
if (hydrogens) {
const h = counts.get('H') || 0;
counts.set('H', h + hydrogens);
}
}
if (counts.has('C')) {
let count = counts.get('C');
molecularFormula += 'C' + (count > 1 ? count : '');
counts.delete('C');
}
if (counts.has('H')) {
let count = counts.get('H');
molecularFormula += 'H' + (count > 1 ? count : '');
counts.delete('H');
}
// TODO: Can we not get keys from counts instead?
let elements = Object.keys(Atom.atomicNumbers).sort();
elements.map((e) => {
if (counts.has(e)) {
let count = counts.get(e);
molecularFormula += e + (count > 1 ? count : '');
}
});
return molecularFormula;
}
/**
* Returns the type of the ringbond (e.g. '=' for a double bond). The ringbond represents the break in a ring introduced when creating the MST. If the two vertices supplied as arguments are not part of a common ringbond, the method returns null.
*
* @param {Vertex} vertexA A vertex.
* @param {Vertex} vertexB A vertex.
* @returns {(String|null)} Returns the ringbond type or null, if the two supplied vertices are not connected by a ringbond.
*/
getRingbondType(vertexA, vertexB) {
// Checks whether the two vertices are the ones connecting the ring
// and what the bond type should be.
if (vertexA.value.getRingbondCount() < 1 || vertexB.value.getRingbondCount() < 1) {
return null;
}
for (let i = 0; i < vertexA.value.ringbonds.length; i++) {
for (let j = 0; j < vertexB.value.ringbonds.length; j++) {
// if(i != j) continue;
if (vertexA.value.ringbonds[i].id === vertexB.value.ringbonds[j].id) {
// If the bonds are equal, it doesn't matter which bond is returned.
// if they are not equal, return the one that is not the default ('-')
if (vertexA.value.ringbonds[i].bondType === '-') {
return vertexB.value.ringbonds[j].bond;
}
else {
return vertexA.value.ringbonds[i].bond;
}
}
}
}
return null;
}
initDraw(data, themeName, infoOnly, highlight_atoms) {
this.data = data;
this.infoOnly = infoOnly;
this.ringIdCounter = 0;
this.ringConnectionIdCounter = 0;
this.graph = new Graph(data, this.opts.isomeric);
this.rings = [];
this.ringConnections = [];
this.originalRings = [];
this.originalRingConnections = [];
this.bridgedRing = false;
// Reset those, in case the previous drawn SMILES had a dangling \ or /
this.doubleBondConfigCount = null;
this.doubleBondConfig = null;
this.highlight_atoms = highlight_atoms;
this.initRings();
this.initHydrogens();
}
processGraph() {
this.position();
this.fixDoubleBondStereo();
// Restore the ring information (removes bridged rings and replaces them with the original, multiple, rings)
this.restoreRingInformation();
// Atoms bonded to the same ring atom
this.resolvePrimaryOverlaps();
let overlapScore = this.getOverlapScore();
this.totalOverlapScore = this.getOverlapScore().total;
for (let o = 0; o < this.opts.overlapResolutionIterations; o++) {
for (let i = 0; i < this.graph.edges.length; i++) {
let edge = this.graph.edges[i];
if (this.isEdgeRotatable(edge)) {
let subTreeDepthA = this.graph.getTreeDepth(edge.sourceId, edge.targetId);
let subTreeDepthB = this.graph.getTreeDepth(edge.targetId, edge.sourceId);
// Only rotate the shorter subtree
let a = edge.targetId;
let b = edge.sourceId;
if (subTreeDepthA > subTreeDepthB) {
a = edge.sourceId;
b = edge.targetId;
}
let subTreeOverlap = this.getSubtreeOverlapScore(b, a, overlapScore.vertexScores);
if (subTreeOverlap.value > this.opts.overlapSensitivity) {
let vertexA = this.graph.vertices[a];
let vertexB = this.graph.vertices[b];
let neighboursB = vertexB.getNeighbours(a);
if (neighboursB.length === 1) {
let neighbour = this.graph.vertices[neighboursB[0]];
let angle = neighbour.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper.toRad(120));
this.rotateSubtree(neighbour.id, vertexB.id, angle, vertexB.position);
// If the new overlap is bigger, undo change
let newTotalOverlapScore = this.getOverlapScore().total;
if (newTotalOverlapScore > this.totalOverlapScore) {
this.rotateSubtree(neighbour.id, vertexB.id, -angle, vertexB.position);
}
else {
this.totalOverlapScore = newTotalOverlapScore;
}
}
else if (neighboursB.length === 2) {
// Switch places / sides
// Here we only try to rotate a simple ring substituent.
// If both ends of the bond are already inside rings, this code gives up.
// That means it will not help with a ring attached to another ring
// layouts, which is why a later dedicated pass was added
if (vertexB.value.rings.length !== 0 && vertexA.value.rings.length !== 0) {
continue;
}
let neighbourA = this.graph.vertices[neighboursB[0]];
let neighbourB = this.graph.vertices[neighboursB[1]];
if (neighbourA.value.rings.length === 1 && neighbourB.value.rings.length === 1) {
// We only want the case where these two neighbours belong to the same ring.
// In practice, this means vertexB is acting like the attachment point for one ring.
if (neighbourA.value.rings[0] !== neighbourB.value.rings[0]) {
continue;
}
let ringId = neighbourA.value.rings[0];
// only handle rings that have a single
// connection to the rest of the structure. If the ring has multiple exits,
// rotating it here becomes much less predictable
if (this.getRingExternalConnectionCount(ringId) !== 1) {
continue;
}
let bestAngle = 0.0;
let bestOverlap = this.totalOverlapScore;
let ring = this.getRing(ringId);
let stepAngle = MathHelper.centralAngle(ring.getSize());
let maxSteps = Math.max(1, Math.floor(ring.getSize() / 2));
// TODO: speedup by rotating by stepAngle each iteration instead
// of resetting to origin and rotating by step*stepAngle. Then do
// one final rotation to the best position. (See PR #237 review.)
for (let step = 1; step <= maxSteps; step++) {
let angle = stepAngle * step;
// Try roatation in one direction
this.rotateSubtree(vertexB.id, vertexA.id, angle, vertexB.position);
let newTotalOverlapScore = this.getOverlapScore().total;
if (newTotalOverlapScore < bestOverlap) {
bestOverlap = newTotalOverlapScore;
bestAngle = angle;
}
// Try in the other direction (twice to revert previous one)
this.rotateSubtree(vertexB.id, vertexA.id, -angle, vertexB.position);
this.rotateSubtree(vertexB.id, vertexA.id, -angle, vertexB.position);
newTotalOverlapScore = this.getOverlapScore().total;
if (newTotalOverlapScore < bestOverlap) {
bestOverlap = newTotalOverlapScore;
bestAngle = -angle;
}
// restore the original before testing the next angle.
this.rotateSubtree(vertexB.id, vertexA.id, angle, vertexB.position);
}
// only keep a rotation if we actually found an orientation that improved
// the global overlap score
if (bestAngle !== 0.0) {
this.rotateSubtree(vertexB.id, vertexA.id, bestAngle, vertexB.position);
this.totalOverlapScore = bestOverlap;
}
}
else if (neighbourA.value.rings.length !== 0 || neighbourB.value.rings.length !== 0) {
continue;
}
else {
let angleA = neighbourA.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper.toRad(120));
let angleB = neighbourB.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper.toRad(120));
this.rotateSubtree(neighbourA.id, vertexB.id, angleA, vertexB.position);
this.rotateSubtree(neighbourB.id, vertexB.id, angleB, vertexB.position);
let newTotalOverlapScore = this.getOverlapScore().total;
if (newTotalOverlapScore > this.totalOverlapScore) {
this.rotateSubtree(neighbourA.id, vertexB.id, -angleA, vertexB.position);
this.rotateSubtree(neighbourB.id, vertexB.id, -angleB, vertexB.position);
}
else {
this.totalOverlapScore = newTotalOverlapScore;
}
}
}
overlapScore = this.getOverlapScore();
}
}
}
}
this.resolveSecondaryOverlaps(overlapScore.scores);
this.resolveRigidRingOverlaps();
overlapScore = this.getOverlapScore();
this.resolveSecondaryOverlaps(overlapScore.scores);
if (this.opts.isomeric) {
this.annotateStereochemistry();
}
// Initialize pseudo elements or shortcuts
if (this.opts.compactDrawing && this.opts.atomVisualization === 'default') {
this.initPseudoElements();
}
this.rotateDrawing();
}
/**
* Initializes rings and ringbonds for the current molecule.
*/
initRings() {
let openBonds = new Map();
// Close the open ring bonds (spanning tree -> graph)
for (let i = this.graph.vertices.length - 1; i >= 0; i--) {
let vertex = this.graph.vertices[i];
if (vertex.value.ringbonds.length === 0) {
continue;
}
for (let j = 0; j < vertex.value.ringbonds.length; j++) {
let ringbondId = vertex.value.ringbonds[j].id;
let ringbondBond = vertex.value.ringbonds[j].bond;
// If the other ringbond id has not been discovered,
// add it to the open bonds map and continue.
// if the other ringbond id has already been discovered,
// create a bond between the two atoms.
if (!openBonds.has(ringbondId)) {
openBonds.set(ringbondId, [vertex.id, ringbondBond]);
}
else {
let sourceVertexId = vertex.id;
let targetVertexId = openBonds.get(ringbondId)[0];
let targetRingbondBond = openBonds.get(ringbondId)[1];
let edge = new Edge(sourceVertexId, targetVertexId, 1);
edge.setBondType(targetRingbondBond || ringbondBond || '-');
let edgeId = this.graph.addEdge(edge);
let targetVertex = this.graph.vertices[targetVertexId];
vertex.addRingbondChild(targetVertexId, j);
vertex.value.addNeighbouringElement(targetVertex.value.element);
// Find the ringbond index on the TARGET vertex (not the source)
let targetRingbondIdx = 0;
for (let k = 0; k < targetVertex.value.ringbonds.length; k++) {
if (targetVertex.value.ringbonds[k].id === ringbondId) {
targetRingbondIdx = k;
break;
}
}
targetVertex.addRingbondChild(sourceVertexId, targetRingbondIdx);
targetVertex.value.addNeighbouringElement(vertex.value.element);
vertex.edges.push(edgeId);
targetVertex.edges.push(edgeId);
openBonds.delete(ringbondId);
}
}
}
// Get the rings in the graph (the SSSR)
let rings = SSSR.getRings(this.graph, this.opts.experimentalSSSR);
if (rings === null || rings.length === 0) {
return;
}
for (let i = 0; i < rings.length; i++) {
let ringVertices = [...rings[i]];
let ringId = this.addRing(new Ring(ringVertices));
// Add the ring to the atoms
for (let j = 0; j < ringVertices.length; j++) {
this.graph.vertices[ringVertices[j]].value.rings.push(ringId);
}
}
// Find connection between rings
// Check for common vertices and create ring connections. This is a bit
// ugly, but the ringcount is always fairly low (< 100)
for (let i = 0; i < this.rings.length - 1; i++) {
for (let j = i + 1; j < this.rings.length; j++) {
let a = this.rings[i];
let b = this.rings[j];
let ringConnection = new RingConnection(a, b);
// If there are no vertices in the ring connection, then there
// is no ring connection
if (ringConnection.vertices.size > 0) {
this.addRingConnection(ringConnection);
}
}
}
// Add neighbours to the rings
for (let i = 0; i < this.rings.length; i++) {
let ring = this.rings[i];
ring.neighbours = RingConnection.getNeighbours(this.ringConnections, ring.id);
}
// Anchor the ring to one of it's members, so that the ring center will always
// be tied to a single vertex when doing repositionings
for (let i = 0; i < this.rings.length; i++) {
let ring = this.rings[i];
this.graph.vertices[ring.members[0]].value.addAnchoredRing(ring.id);
}
// Backup the ring information to restore after placing the bridged ring.
// This is needed in order to identify aromatic rings and stuff like this in
// rings that are member of the superring.
this.backupRingInformation();
// Replace rings contained by a larger bridged ring with a bridged ring
while (this.rings.length > 0) {
let id = -1;
for (let i = 0; i < this.rings.length; i++) {
let ring = this.rings[i];
if (this.isPartOfBridgedRing(ring.id) && !ring.isBridged) {
id = ring.id;
}
}
if (id === -1) {
break;
}
let ring = this.getRing(id);
let involvedRings = this.getBridgedRingRings(ring.id);
this.bridgedRing = true;
this.createBridgedRing(involvedRings, ring.members[0]);
this.bridgedRing = false;
// Remove the rings
for (let i = 0; i < involvedRings.length; i++) {
this.removeRing(involvedRings[i]);
}
}
}
initHydrogens() {
// Do not draw hydrogens except when they are connected to a stereocenter connected to two or more rings.
if (!this.opts.explicitHydrogens) {
for (let i = 0; i < this.graph.vertices.length; i++) {
let vertex = this.graph.vertices[i];
if (vertex.value.element !== 'H') {
continue;
}
// Hydrogens should have only one neighbour, so just take the first
// Also set hasHydrogen true on connected atom
let neighbour = this.graph.vertices[vertex.neighbours[0]];
neighbour.value.hasHydrogen = true;
if (!neighbour.value.isStereoCenter
|| (neighbour.value.rings.length < 2 && !neighbour.value.bridgedRing)
|| (neighbour.value.bridgedRing && neighbour.value.originalRings.length < 2)
) {
vertex.value.isDrawn = false;
}
}
}
}
/**
* Returns all rings connected by bridged bonds starting from the ring with the supplied ring id.
*
* @param {Number} ringId A ring id.
* @returns {Number[]} An array containing all ring ids of rings part of a bridged ring system.
*/
getBridgedRingRings(ringId) {
let involvedRings = [];
let recurse = (r) => {
let ring = this.getRing(r);
involvedRings.push(r);
for (let i = 0; i < ring.neighbours.length; i++) {
let n = ring.neighbours[i];
if (involvedRings.indexOf(n) === -1 && n !== r && RingConnection.isBridge(this.ringConnections, this.graph.vertices, r, n)) {
recurse(n);
}
}
};
recurse(ringId);
// recurse() is only used for BRIDGED connections (rings that share 3+ atoms)
// but FUSED rings (exactly 2 shared atoms, like in naphtahlene) are left out.
// THis causes issues if the bridged system is laid out by KK. If a fused ring
// shares 2 atoms with the bridges system but isn't included, those 2 atoms
// get positioned by KK, while the rest of the fused rings gets positioned by the
// normal layout algorithm. Both algos fight producing distored drawings
// TODO: change recurse() by making it always recurse when there are two or more
// shared vertices and use a Set instead of indexOf on an array.
// (See PR#237 review)
let changed = true;
while (changed) {
changed = false;
for (let i = 0; i < this.ringConnections.length; i++) {
let rc = this.ringConnections[i];
if (rc.vertices.size < 2) continue;
let hasFirst = involvedRings.indexOf(rc.firstRingId) !== -1;
let hasSecond = involvedRings.indexOf(rc.secondRingId) !== -1;
if (hasFirst && !hasSecond) {
involvedRings.push(rc.secondRingId);
changed = true;
}
else if (hasSecond && !hasFirst) {
involvedRings.push(rc.firstRingId);
changed = true;
}
}
}
return ArrayHelper.unique(involvedRings);
}
/**
* Checks whether or not a ring is part of a bridged ring.
*
* @param {Number} ringId A ring id.
* @returns {Boolean} A boolean indicating whether or not the supplied ring (by id) is part of a bridged ring system.
*/
isPartOfBridgedRing(ringId) {
for (let i = 0; i < this.ringConnections.length; i++) {
if (this.ringConnections[i].containsRing(ringId) && this.ringConnections[i].isBridge(this.graph.vertices)) {
return true;
}
}
return false;
}
/**
* Creates a bridged ring.
*
* @param {Number[]} ringIds An array of ids of rings involved in the bridged ring.
* @param {Number} sourceVertexId The vertex id to start the bridged ring discovery from.
* @returns {Ring} The bridged ring.
*/
createBridgedRing(ringIds, sourceVertexId) {
let ringMembers = new Set();
let vertices = new Set();
let neighbours = new Set();
for (let i = 0; i < ringIds.length; i++) {
let ring = this.getRing(ringIds[i]);
ring.isPartOfBridged = true;
for (let j = 0; j < ring.members.length; j++) {
vertices.add(ring.members[j]);
}
for (let j = 0; j < ring.neighbours.length; j++) {
let id = ring.neighbours[j];
if (ringIds.indexOf(id) === -1) {
neighbours.add(ring.neighbours[j]);
}
}
}
// A vertex is part of the bridged ring if it only belongs to
// one of the rings (or to another ring
// which is not part of the bridged ring).
let leftovers = new Set();
for (let id of vertices) {
let vertex = this.graph.vertices[id];
let intersection = ArrayHelper.intersection(ringIds, vertex.value.rings);
if (vertex.value.rings.length === 1 || intersection.length === 1) {
ringMembers.add(vertex.id);
}
else {
leftovers.add(vertex.id);
}
}
// Vertices can also be part of multiple rings and lay on the bridged ring,
// however, they have to have at least two neighbours that are not part of
// two rings
let insideRing = [];
for (let id of leftovers) {
let vertex = this.graph.vertices[id];
let onRing = false;
for (let j = 0; j < vertex.edges.length; j++) {
if (this.edgeRingCount(vertex.edges[j]) === 1) {
onRing = true;
}
}
if (onRing) {
vertex.value.isBridgeNode = true;
ringMembers.add(vertex.id);
}
else {
vertex.value.isBridge = true;
insideRing.push(vertex.id);
ringMembers.add(vertex.id);
}
}
// Create the ring
let ring = new Ring([...ringMembers]);
this.addRing(ring);
ring.isBridged = true;
ring.insiders = insideRing;
ring.neighbours = [...neighbours];
for (let i = 0; i < ringIds.length; i++) {
ring.rings.push(this.getRing(ringIds[i]).clone());
}
for (let i = 0; i < ring.members.length; i++) {
this.graph.vertices[ring.members[i]].value.bridgedRing = ring.id;
}
// Atoms inside the ring are no longer part of a ring but are now
// associated with the bridged ring
for (let i = 0; i < insideRing.length; i++) {
let vertex = this.graph.vertices[insideRing[i]];
vertex.value.rings = [];
}
// Remove former rings from members of the bridged ring and add the bridged ring
for (let id of ringMembers) {
let vertex = this.graph.vertices[id];
vertex.value.rings = ArrayHelper.removeAll(vertex.value.rings, ringIds);
vertex.value.rings.push(ring.id);
}
// Remove all the ring connections no longer used
for (let i = 0; i < ringIds.length; i++) {
for (let j = i + 1; j < ringIds.length; j++) {
this.removeRingConnectionsBetween(ringIds[i], ringIds[j]);
}
}
// Update the ring connections and add this ring to the neighbours neighbours
for (let id of neighbours) {
let connections = this.getRingConnections(id, ringIds);
for (let j = 0; j < connections.length; j++) {
this.getRingConnection(connections[j]).updateOther(ring.id, id);
}
this.getRing(id).neighbours.push(ring.id);
}
return ring;
}
/**
* Checks whether or not two vertices are in the same ring.
*
* @param {Vertex} vertexA A vertex.
* @param {Vertex} vertexB A vertex.
* @returns {Boolean} A boolean indicating whether or not the two vertices are in the same ring.
*/
areVerticesInSameRing(vertexA, vertexB) {
// This is a little bit lighter (without the array and push) than
// getCommonRings().length > 0
for (let i = 0; i < vertexA.value.rings.length; i++) {
for (let j = 0; j < vertexB.value.rings.length; j++) {
if (vertexA.value.rings[i] === vertexB.value.rings[j]) {
return true;
}
}
}
return false;
}
/**
* Returns an array of ring ids shared by both vertices.
*
* @param {Vertex} vertexA A vertex.
* @param {Vertex} vertexB A vertex.
* @returns {Number[]} An array of ids of rings shared by the two vertices.
*/
getCommonRings(vertexA, vertexB) {
let commonRings = [];
for (let i = 0; i < vertexA.value.rings.length; i++) {
for (let j = 0; j < vertexB.value.rings.length; j++) {
if (vertexA.value.rings[i] == vertexB.value.rings[j]) {
commonRings.push(vertexA.value.rings[i]);
}
}
}
return commonRings;
}
/**
* Returns the aromatic or largest ring shared by the two vertices.
*
* @param {Vertex} vertexA A vertex.
* @param {Vertex} vertexB A vertex.
* @returns {(Ring|null)} If an aromatic common ring exists, that ring, else the largest (non-aromatic) ring, else null.
*/
getLargestOrAromaticCommonRing(vertexA, vertexB) {
let commonRings = this.getCommonRings(vertexA, vertexB);
let maxSize = 0;
let largestCommonRing = null;
for (let i = 0; i < commonRings.length; i++) {
let ring = this.getRing(commonRings[i]);
let size = ring.getSize();
if (ring.isBenzeneLike(this.graph.vertices)) {
return ring;
}
else if (size > maxSize) {
maxSize = size;
largestCommonRing = ring;
}
}
return largestCommonRing;
}
/**
* Returns an array of vertices positioned at a specified location.
*
* @param {Vector2} position The position to search for vertices.
* @param {Number} radius The radius within to search.
* @param {Number} excludeVertexId A vertex id to be excluded from the search results.
* @returns {Number[]} An array containing vertex ids in a given location.
*/
getVerticesAt(position, radius, excludeVertexId) {
let locals = [];
for (let i = 0; i < this.graph.vertices.length; i++) {
let vertex = this.graph.vertices[i];
if (vertex.id === excludeVertexId || !vertex.positioned) {
continue;
}
let distance = position.distanceSq(vertex.position);
if (distance <= radius * radius) {
locals.push(vertex.id);
}
}
return locals;
}
/**
* Returns the closest vertex (connected as well as unconnected).
*
* @param {Vertex} vertex The vertex of which to find the closest other vertex.
* @returns {Vertex} The closest vertex.
*/
getClosestVertex(vertex) {
let minDist = 99999;
let minVertex = null;
for (let i = 0; i < this.graph.vertices.length; i++) {
let v = this.graph.vertices[i];
if (v.id === vertex.id) {
continue;
}
let distSq = vertex.position.distanceSq(v.position);
if (distSq < minDist) {
minDist = distSq;
minVertex = v;
}
}
return minVertex;
}
/**
* Add a ring to this representation of a molecule.
*
* @param {Ring} ring A new ring.
* @returns {Number} The ring id of the new ring.
*/
addRing(ring) {
ring.id = this.ringIdCounter++;
this.rings.push(ring);
return ring.id;
}
/**
* Removes a ring from the array of rings associated with the current molecule.
*
* @param {Number} ringId A ring id.
*/
removeRing(ringId) {
this.rings = this.rings.filter(function(item) {
return item.id !== ringId;
});
// Also remove ring connections involving this ring
this.ringConnections = this.ringConnections.filter(function(item) {
return item.firstRingId !== ringId && item.secondRingId !== ringId;
});
// Remove the ring as neighbour of other rings
for (let i = 0; i < this.rings.length; i++) {
let r = this.rings[i];
r.neighbours = r.neighbours.filter(function(item) {
return item !== ringId;
});
}
}
/**
* Gets a ring object from the array of rings associated with the current molecule by its id. The ring id is not equal to the index, since rings can be added and removed when processing bridged rings.
*
* @param {Number} ringId A ring id.
* @returns {Ring} A ring associated with the current molecule.
*/
getRing(ringId) {
for (let i = 0; i < this.rings.length; i++) {
if (this.rings[i].id == ringId) {
return this.rings[i];
}
}
}
/**
* Add a ring conne