smiles-drawer
Version:
A SMILES drawer and parser. Generate molecular structure depictions in pure JavaScript.
1,504 lines (1,255 loc) • 105 kB
JavaScript
//@ts-check
const MathHelper = require('./MathHelper')
const ArrayHelper = require('./ArrayHelper')
const Vector2 = require('./Vector2')
const Line = require('./Line')
const Vertex = require('./Vertex')
const Edge = require('./Edge')
const Atom = require('./Atom')
const Ring = require('./Ring')
const RingConnection = require('./RingConnection')
const CanvasWrapper = require('./CanvasWrapper')
const Graph = require('./Graph')
const SSSR = require('./SSSR')
/**
* 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.
*/
class Drawer {
/**
* 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,
bondThickness: 0.6,
bondLength: 15,
shortBondLength: 0.85,
bondSpacing: 0.18 * 15,
atomVisualization: 'default',
isomeric: true,
debug: false,
terminalCarbons: false,
explicitHydrogens: false,
overlapSensitivity: 0.42,
overlapResolutionIterations: 1,
compactDrawing: true,
fontSizeLarge: 5,
fontSizeSmall: 3,
padding: 20.0,
themes: {
dark: {
C: '#fff',
O: '#e74c3c',
N: '#3498db',
F: '#27ae60',
CL: '#16a085',
BR: '#d35400',
I: '#8e44ad',
P: '#d35400',
S: '#f1c40f',
B: '#e67e22',
SI: '#e67e22',
H: '#fff',
BACKGROUND: '#141414'
},
light: {
C: '#222',
O: '#e74c3c',
N: '#3498db',
F: '#27ae60',
CL: '#16a085',
BR: '#d35400',
I: '#8e44ad',
P: '#d35400',
S: '#f1c40f',
B: '#e67e22',
SI: '#e67e22',
H: '#222',
BACKGROUND: '#fff'
}
}
};
this.opts = this.extend(true, this.defaultOptions, options);
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;
}
/**
* A helper method to extend the default options with user supplied ones.
*/
extend() {
let that = this;
let extended = {};
let deep = false;
let i = 0;
let length = arguments.length;
if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') {
deep = arguments[0];
i++;
}
let merge = function (obj) {
for (var prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
extended[prop] = that.extend(true, extended[prop], obj[prop]);
} else {
extended[prop] = obj[prop];
}
}
}
};
for (; i < length; i++) {
let obj = arguments[i];
merge(obj);
}
return extended;
};
/**
* Draws the parsed smiles data to a canvas element.
*
* @param {Object} data The tree returned by the smiles parser.
* @param {(String|HTMLElement)} 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.data = data;
this.infoOnly = infoOnly;
if (!this.infoOnly) {
this.canvasWrapper = new CanvasWrapper(target, this.opts.themes[themeName], this.opts);
}
this.ringIdCounter = 0;
this.ringConnectionIdCounter = 0;
this.graph = new Graph(data, this.opts.isomeric);
this.rings = Array();
this.ringConnections = Array();
this.originalRings = Array();
this.originalRingConnections = Array();
this.bridgedRing = false;
// Reset those, in case the previous drawn SMILES had a dangling \ or /
this.doubleBondConfigCount = null;
this.doubleBondConfig = null;
this.initRings();
this.initHydrogens();
if (!this.infoOnly) {
this.position();
// 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 (var o = 0; o < this.opts.overlapResolutionIterations; o++) {
for (var 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
// If vertex a is in a ring, do nothing
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) {
// Both neighbours in same ring. TODO: does this create problems with wedges? (up = down and vice versa?)
if (neighbourA.value.rings[0] !== neighbourB.value.rings[0]) {
continue;
}
// TODO: Rotate circle
} 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);
if (this.opts.isomeric) {
this.annotateStereochemistry();
}
// Initialize pseudo elements or shortcuts
if (this.opts.compactDrawing && this.opts.atomVisualization === 'default') {
this.initPseudoElements();
}
this.rotateDrawing();
// 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();
}
}
/**
* 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() {
let bridgedRings = Array();
for (var i = 0; i < this.rings.length; i++) {
if (this.rings[i].isBridged) {
bridgedRings.push(this.rings[i]);
}
}
return bridgedRings;
}
/**
* Returns an array containing all fused rings associated with this molecule.
*
* @returns {Ring[]} An array containing all fused rings associated with this molecule.
*/
getFusedRings() {
let fusedRings = Array();
for (var i = 0; i < this.rings.length; i++) {
if (this.rings[i].isFused) {
fusedRings.push(this.rings[i]);
}
}
return fusedRings;
}
/**
* Returns an array containing all spiros associated with this molecule.
*
* @returns {Ring[]} An array containing all spiros associated with this molecule.
*/
getSpiros() {
let spiros = Array();
for (var i = 0; i < this.rings.length; i++) {
if (this.rings[i].isSpiro) {
spiros.push(this.rings[i]);
}
}
return spiros;
}
/**
* 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 (var 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 (var i = 0; i < this.graph.vertices.length; i++) {
let vertexA = this.graph.vertices[i];
if (!vertexA.value.isDrawn) {
continue;
}
for (var 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 (var 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 (var 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 (var 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() {
let molecularFormula = '';
let counts = new Map();
// Initialize element count
for (var i = 0; i < this.graph.vertices.length; i++) {
let atom = this.graph.vertices[i].value;
if (counts.has(atom.element)) {
counts.set(atom.element, counts.get(atom.element) + 1);
} else {
counts.set(atom.element, 1);
}
// Hydrogens attached to a chiral center were added as vertices,
// those in non chiral brackets are added here
if (atom.bracket && !atom.bracket.chirality) {
if (counts.has('H')) {
counts.set('H', counts.get('H') + atom.bracket.hcount);
} else {
counts.set('H', atom.bracket.hcount);
}
}
// Add the implicit hydrogens according to valency, exclude
// bracket atoms as they were handled and always have the number
// of hydrogens specified explicitly
if (!atom.bracket) {
let nHydrogens = Atom.maxBonds[atom.element] - atom.bondCount;
if (atom.isPartOfAromaticRing) {
nHydrogens--;
}
if (counts.has('H')) {
counts.set('H', counts.get('H') + nHydrogens);
} else {
counts.set('H', nHydrogens);
}
}
}
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');
}
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 (var i = 0; i < vertexA.value.ringbonds.length; i++) {
for (var 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;
}
/**
* Initializes rings and ringbonds for the current molecule.
*/
initRings() {
let openBonds = new Map();
// Close the open ring bonds (spanning tree -> graph)
for (var i = this.graph.vertices.length - 1; i >= 0; i--) {
let vertex = this.graph.vertices[i];
if (vertex.value.ringbonds.length === 0) {
continue;
}
for (var 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);
targetVertex.addRingbondChild(sourceVertexId, j);
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);
if (rings === null) {
return;
}
for (var i = 0; i < rings.length; i++) {
let ringVertices = [...rings[i]];
let ringId = this.addRing(new Ring(ringVertices));
// Add the ring to the atoms
for (var 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 (var i = 0; i < this.rings.length - 1; i++) {
for (var 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 (var 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 (var 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 (var 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]);
// Remove the rings
for (var 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 (var 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 = Array();
let that = this;
let recurse = function (r) {
let ring = that.getRing(r);
involvedRings.push(r);
for (var i = 0; i < ring.neighbours.length; i++) {
let n = ring.neighbours[i];
if (involvedRings.indexOf(n) === -1 &&
n !== r &&
RingConnection.isBridge(that.ringConnections, that.graph.vertices, r, n)) {
recurse(n);
}
}
};
recurse(ringId);
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 (var 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 (var i = 0; i < ringIds.length; i++) {
let ring = this.getRing(ringIds[i]);
ring.isPartOfBridged = true;
for (var j = 0; j < ring.members.length; j++) {
vertices.add(ring.members[j]);
}
for (var 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 tmp = Array();
let insideRing = Array();
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;
ringMembers.add(vertex.id);
}
}
// Create the ring
let ring = new Ring([...ringMembers]);
ring.isBridged = true;
ring.neighbours = [...neighbours];
for (var i = 0; i < ringIds.length; i++) {
ring.rings.push(this.getRing(ringIds[i]).clone());
}
this.addRing(ring);
for (var 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 (var i = 0; i < insideRing.length; i++) {
let vertex = this.graph.vertices[insideRing[i]];
vertex.value.rings = Array();
}
// 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 (var i = 0; i < ringIds.length; i++) {
for (var 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 (var 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 (var i = 0; i < vertexA.value.rings.length; i++) {
for (var 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 = Array();
for (var i = 0; i < vertexA.value.rings.length; i++) {
for (var 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 (var 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 = Array();
for (var 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 (var 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 (var 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 (var i = 0; i < this.rings.length; i++) {
if (this.rings[i].id == ringId) {
return this.rings[i];
}
}
}
/**
* Add a ring connection to this representation of a molecule.
*
* @param {RingConnection} ringConnection A new ringConnection.
* @returns {Number} The ring connection id of the new ring connection.
*/
addRingConnection(ringConnection) {
ringConnection.id = this.ringConnectionIdCounter++;
this.ringConnections.push(ringConnection);
return ringConnection.id;
}
/**
* Removes a ring connection from the array of rings connections associated with the current molecule.
*
* @param {Number} ringConnectionId A ring connection id.
*/
removeRingConnection(ringConnectionId) {
this.ringConnections = this.ringConnections.filter(function (item) {
return item.id !== ringConnectionId;
});
}
/**
* Removes all ring connections between two vertices.
*
* @param {Number} vertexIdA A vertex id.
* @param {Number} vertexIdB A vertex id.
*/
removeRingConnectionsBetween(vertexIdA, vertexIdB) {
let toRemove = Array();
for (var i = 0; i < this.ringConnections.length; i++) {
let ringConnection = this.ringConnections[i];
if (ringConnection.firstRingId === vertexIdA && ringConnection.secondRingId === vertexIdB ||
ringConnection.firstRingId === vertexIdB && ringConnection.secondRingId === vertexIdA) {
toRemove.push(ringConnection.id);
}
}
for (var i = 0; i < toRemove.length; i++) {
this.removeRingConnection(toRemove[i]);
}
}
/**
* Get a ring connection with a given id.
*
* @param {Number} id
* @returns {RingConnection} The ring connection with the specified id.
*/
getRingConnection(id) {
for (var i = 0; i < this.ringConnections.length; i++) {
if (this.ringConnections[i].id == id) {
return this.ringConnections[i];
}
}
}
/**
* Get the ring connections between a ring and a set of rings.
*
* @param {Number} ringId A ring id.
* @param {Number[]} ringIds An array of ring ids.
* @returns {Number[]} An array of ring connection ids.
*/
getRingConnections(ringId, ringIds) {
let ringConnections = Array();
for (var i = 0; i < this.ringConnections.length; i++) {
let rc = this.ringConnections[i];
for (var j = 0; j < ringIds.length; j++) {
let id = ringIds[j];
if (rc.firstRingId === ringId && rc.secondRingId === id ||
rc.firstRingId === id && rc.secondRingId === ringId) {
ringConnections.push(rc.id);
}
}
}
return ringConnections;
}
/**
* Returns the overlap score of the current molecule based on its positioned vertices. The higher the score, the more overlaps occur in the structure drawing.
*
* @returns {Object} Returns the total overlap score and the overlap score of each vertex sorted by score (higher to lower). Example: { total: 99, scores: [ { id: 0, score: 22 }, ... ] }
*/
getOverlapScore() {
let total = 0.0;
let overlapScores = new Float32Array(this.graph.vertices.length);
for (var i = 0; i < this.graph.vertices.length; i++) {
overlapScores[i] = 0;
}
for (var i = 0; i < this.graph.vertices.length; i++) {
var j = this.graph.vertices.length;
while (--j > i) {
let a = this.graph.vertices[i];
let b = this.graph.vertices[j];
if (!a.value.isDrawn || !b.value.isDrawn) {
continue;
}
let dist = Vector2.subtract(a.position, b.position).lengthSq();
if (dist < this.opts.bondLengthSq) {
let weighted = (this.opts.bondLength - Math.sqrt(dist)) / this.opts.bondLength;
total += weighted;
overlapScores[i] += weighted;
overlapScores[j] += weighted;
}
}
}
let sortable = Array();
for (var i = 0; i < this.graph.vertices.length; i++) {
sortable.push({
id: i,
score: overlapScores[i]
});
}
sortable.sort(function (a, b) {
return b.score - a.score;
});
return {
total: total,
scores: sortable,
vertexScores: overlapScores
};
}
/**
* When drawing a double bond, choose the side to place the double bond. E.g. a double bond should always been drawn inside a ring.
*
* @param {Vertex} vertexA A vertex.
* @param {Vertex} vertexB A vertex.
* @param {Vector2[]} sides An array containing the two normals of the line spanned by the two provided vertices.
* @returns {Object} Returns an object containing the following information: {
totalSideCount: Counts the sides of each vertex in the molecule, is an array [ a, b ],
totalPosition: Same as position, but based on entire molecule,
sideCount: Counts the sides of each neighbour, is an array [ a, b ],
position: which side to position the second bond, is 0 or 1, represents the index in the normal array. This is based on only the neighbours
anCount: the number of neighbours of vertexA,
bnCount: the number of neighbours of vertexB
}
*/
chooseSide(vertexA, vertexB, sides) {
// Check which side has more vertices
// Get all the vertices connected to the both ends
let an = vertexA.getNeighbours(vertexB.id);
let bn = vertexB.getNeighbours(vertexA.id);
let anCount = an.length;
let bnCount = bn.length;
// All vertices connected to the edge vertexA to vertexB
let tn = ArrayHelper.merge(an, bn);
// Only considering the connected vertices
let sideCount = [0, 0];
for (var i = 0; i < tn.length; i++) {
let v = this.graph.vertices[tn[i]].position;
if (v.sameSideAs(vertexA.position, vertexB.position, sides[0])) {
sideCount[0]++;
} else {
sideCount[1]++;
}
}
// Considering all vertices in the graph, this is to resolve ties
// from the above side counts
let totalSideCount = [0, 0];
for (var i = 0; i < this.graph.vertices.length; i++) {
let v = this.graph.vertices[i].position;
if (v.sameSideAs(vertexA.position, vertexB.position, sides[0])) {
totalSideCount[0]++;
} else {
totalSideCount[1]++;
}
}
return {
totalSideCount: totalSideCount,
totalPosition: totalSideCount[0] > totalSideCount[1] ? 0 : 1,
sideCount: sideCount,
position: sideCount[0] > sideCount[1] ? 0 : 1,
anCount: anCount,
bnCount: bnCount
};
}
/**
* Sets the center for a ring.
*
* @param {Ring} ring A ring.
*/
setRingCenter(ring) {
let ringSize = ring.getSize();
let total = new Vector2(0, 0);
for (var i = 0; i < ringSize; i++) {
total.add(this.graph.vertices[ring.members[i]].position);
}
ring.center = total.divide(ringSize);
}
/**
* Gets the center of a ring contained within a bridged ring and containing a given vertex.
*
* @param {Ring} ring A bridged ring.
* @param {Vertex} vertex A vertex.
* @returns {Vector2} The center of the subring that containing the vertex.
*/
getSubringCenter(ring, vertex) {
let rings = vertex.value.originalRings;
let center = ring.center;
let smallest = Number.MAX_VALUE;
// Always get the smallest ring.
for (var i = 0; i < rings.length; i++) {
for (var j = 0; j < ring.rings.length; j++) {
if (rings[i] === ring.rings[j].id) {
if (ring.rings[j].getSize() < smallest) {
center = ring.rings[j].center;
smallest = ring.rings[j].getSize();
}
}
}
}
return center;
}
/**
* Draw the actual edges as bonds to the canvas.
*
* @param {Boolean} debug A boolean indicating whether or not to draw debug helpers.
*/
drawEdges(debug) {
let that = this;
let drawn = Array(this.graph.edges.length);
drawn.fill(false);
this.graph.traverseBF(0, function (vertex) {
let edges = that.graph.getEdges(vertex.id);
for (var i = 0; i < edges.length; i++) {
let edgeId = edges[i];
if (!drawn[edgeId]) {
drawn[edgeId] = true;
that.drawEdge(edgeId, debug);
}
}
});
// Draw ring for implicitly defined aromatic rings
if (!this.bridgedRing) {
for (var i = 0; i < this.rings.length; i++) {
let ring = this.rings[i];
if (this.isRingAromatic(ring)) {
this.canvasWrapper.drawAromaticityRing(ring);
}
}
}
}
/**
* Draw the an edge as a bonds to the canvas.
*
* @param {Number} edgeId An edge id.
* @param {Boolean} debug A boolean indicating whether or not to draw debug helpers.
*/
drawEdge(edgeId, debug) {
let that = this;
let edge = this.graph.edges[edgeId];
let vertexA = this.graph.vertices[edge.sourceId];
let vertexB = this.graph.vertices[edge.targetId];
let elementA = vertexA.value.element;
let elementB = vertexB.value.element;
if ((!vertexA.value.isDrawn || !vertexB.value.isDrawn) && this.opts.atomVisualization === 'default') {
return;
}
let a = vertexA.position;
let b = vertexB.position;
let normals = this.getEdgeNormals(edge);
// Create a point on each side of the line
let sides = ArrayHelper.clone(normals);
sides[0].multiplyScalar(10).add(a);
sides[1].multiplyScalar(10).add(a);
if (edge.bondType === '=' || this.getRingbondType(vertexA, vertexB) === '=' ||
(edge.isPartOfAromaticRing && this.bridgedRing)) {
// Always draw double bonds inside the ring
let inRing = this.areVerticesInSameRing(vertexA, vertexB);
let s = this.chooseSide(vertexA, vertexB, sides);
if (inRing) {
// Always draw double bonds inside a ring
// if the bond is shared by two rings, it is drawn in the larger
// problem: smaller ring is aromatic, bond is still drawn in larger -> fix this
let lcr = this.getLargestOrAromaticCommonRing(vertexA, vertexB);
let center = lcr.center;
normals[0].multiplyScalar(that.opts.bondSpacing);
normals[1].multiplyScalar(that.opts.bondSpacing);
// Choose the normal that is on the same side as the center
let line = null;
if (center.sameSideAs(vertexA.position, vertexB.position, Vector2.add(a, normals[0]))) {
line = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
} else {
line = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
}
line.shorten(this.opts.bondLength - this.opts.shortBondLength * this.opts.bondLength);
// The shortened edge
if (edge.isPartOfAromaticRing) {
this.canvasWrapper.drawLine(line, true);
} else {
this.canvasWrapper.drawLine(line);
}
// The normal edge
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else if (edge.center || vertexA.isTerminal() && vertexB.isTerminal()) {
normals[0].multiplyScalar(that.opts.halfBondSpacing);
normals[1].multiplyScalar(that.opts.halfBondSpacing);
let lineA = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
let lineB = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
this.canvasWrapper.drawLine(lineA);
this.canvasWrapper.drawLine(lineB);
} else if (s.anCount == 0 && s.bnCount > 1 || s.bnCount == 0 && s.anCount > 1) {
// Both lines are the same length here
// Add the spacing to the edges (which are of unit length)
normals[0].multiplyScalar(that.opts.halfBondSpacing);
normals[1].multiplyScalar(that.opts.halfBondSpacing);
let lineA = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
let lineB = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
this.canvasWrapper.drawLine(lineA);
this.canvasWrapper.drawLine(lineB);
} else if (s.sideCount[0] > s.sideCount[1]) {
normals[0].multiplyScalar(that.opts.bondSpacing);
normals[1].multiplyScalar(that.opts.bondSpacing);
let line = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
line.shorten(this.opts.bondLength - this.opts.shortBondLength * this.opts.bondLength);
this.canvasWrapper.drawLine(line);
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else if (s.sideCount[0] < s.sideCount[1]) {
normals[0].multiplyScalar(that.opts.bondSpacing);
normals[1].multiplyScalar(that.opts.bondSpacing);
let line = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
line.shorten(this.opts.bondLength - this.opts.shortBondLength * this.opts.bondLength);
this.canvasWrapper.drawLine(line);
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else if (s.totalSideCount[0] > s.totalSideCount[1]) {
normals[0].multiplyScalar(that.opts.bondSpacing);
normals[1].multiplyScalar(that.opts.bondSpacing);
let line = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
line.shorten(this.opts.bondLength - this.opts.shortBondLength * this.opts.bondLength);
this.canvasWrapper.drawLine(line);
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else if (s.totalSideCount[0] <= s.totalSideCount[1]) {
normals[0].multiplyScalar(that.opts.bondSpacing);
normals[1].multiplyScalar(that.opts.bondSpacing);
let line = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
line.shorten(this.opts.bondLength - this.opts.shortBondLength * this.opts.bondLength);
this.canvasWrapper.drawLine(line);
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else {
}
} else if (edge.bondType === '#') {
normals[0].multiplyScalar(that.opts.bondSpacing / 1.5);
normals[1].multiplyScalar(that.opts.bondSpacing / 1.5);
let lineA = new Line(Vector2.add(a, normals[0]), Vector2.add(b, normals[0]), elementA, elementB);
let lineB = new Line(Vector2.add(a, normals[1]), Vector2.add(b, normals[1]), elementA, elementB);
this.canvasWrapper.drawLine(lineA);
this.canvasWrapper.drawLine(lineB);
this.canvasWrapper.drawLine(new Line(a, b, elementA, elementB));
} else if (edge.bondType === '.') {
// TODO: Something... maybe... version 2?
} else {
let isChiralCenterA = vertexA.value.isStereoCenter;
let isChiralCenterB = vertexB.value.isStereoCenter;
if (edge.wedge === 'up') {
this.canvasWrapper.drawWedge(new Line(a, b, elementA, elementB, isChiralCenterA, isChiralCenterB));
} else if (edge.wedge === 'down') {
this.canvasWrapper.drawDashedWedge(new Line(a, b, elementA, elementB, isChiralCenterA, isChiralCenterB));
} else {