mathpix-markdown-it
Version:
Mathpix-markdown-it is an open source implementation of the mathpix-markdown spec written in Typescript. It relies on the following open source libraries: MathJax v3 (to render math with SVGs), markdown-it (for standard Markdown parsing)
1,165 lines • 127 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var MathHelper_1 = require("./MathHelper");
var ArrayHelper_1 = require("./ArrayHelper");
var Vector2_1 = require("./Vector2");
var Line_1 = require("./Line");
var Edge_1 = require("./Edge");
var Atom_1 = require("./Atom");
var Ring_1 = require("./Ring");
var RingConnection_1 = require("./RingConnection");
var CanvasWrapper_1 = require("./CanvasWrapper");
var Graph_1 = require("./Graph");
var SSSR_1 = require("./SSSR");
var ThemeManager_1 = require("./ThemeManager");
;
;
/**
* 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.
*/
var Drawer = /** @class */ (function () {
/**
* The constructor for the class SmilesDrawer.
*
* @param {Object} options An object containing custom values for different options. It is merged with the default options.
*/
function Drawer(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.8,
bondSpacing: 0.18 * 15,
dCircle: 2,
atomVisualization: 'default',
ringVisualization: 'default',
ringAromaticVisualization: 'default',
isomeric: true,
debug: false,
terminalCarbons: false,
explicitHydrogens: true,
overlapSensitivity: 0.42,
overlapResolutionIterations: 1,
compactDrawing: false,
fontSizeLarge: 5,
fontSizeSmall: 3,
padding: 5.0,
experimentalSSSR: false,
kkThreshold: 0.1,
kkInnerThreshold: 0.1,
kkMaxIteration: 20000,
kkMaxInnerIteration: 50,
kkMaxEnergy: 1e9,
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'
}
}
};
// @ts-ignore
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.
*/
Drawer.prototype.extend = function () {
var that = this;
var extended = {};
var deep = false;
var i = 0;
var length = arguments.length;
if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') {
deep = arguments[0];
i++;
}
var 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]') {
// @ts-ignore
extended[prop] = that.extend(true, extended[prop], obj[prop]);
// extended[prop] = that.extend();
}
else {
extended[prop] = obj[prop];
}
}
}
};
for (; i < length; i++) {
var 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.
*/
Drawer.prototype.draw = function (data, target, themeName, infoOnly) {
if (themeName === void 0) { themeName = 'light'; }
if (infoOnly === void 0) { infoOnly = false; }
this.initDraw(data, themeName, infoOnly);
if (!this.infoOnly) {
this.themeManager = new ThemeManager_1.default(this.opts.themes, themeName);
this.canvasWrapper = new CanvasWrapper_1.default(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.log(this.graph);
console.log(this.rings);
console.log(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.
*/
Drawer.prototype.edgeRingCount = function (edgeId) {
var edge = this.graph.edges[edgeId];
var a = this.graph.vertices[edge.sourceId];
var 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.
*/
Drawer.prototype.getBridgedRings = function () {
var 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.
*/
Drawer.prototype.getFusedRings = function () {
var 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.
*/
Drawer.prototype.getSpiros = function () {
var 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.
*/
Drawer.prototype.printRingInfo = function () {
var result = '';
for (var i = 0; i < this.rings.length; i++) {
var 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.
*/
Drawer.prototype.rotateDrawing = function () {
// Rotate the vertices to make the molecule align horizontally
// Find the longest distance
var a = 0;
var b = 0;
var maxDist = 0;
for (var i = 0; i < this.graph.vertices.length; i++) {
var vertexA = this.graph.vertices[i];
if (!vertexA.value.isDrawn) {
continue;
}
for (var j = i + 1; j < this.graph.vertices.length; j++) {
var vertexB = this.graph.vertices[j];
if (!vertexB.value.isDrawn) {
continue;
}
var dist = vertexA.position.distanceSq(vertexB.position);
if (dist > maxDist) {
maxDist = dist;
a = i;
b = j;
}
}
}
var angle = -Vector2_1.default.subtract(this.graph.vertices[a].position, this.graph.vertices[b].position).angle();
if (!isNaN(angle)) {
// Round to 30 degrees
var 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.
*/
Drawer.prototype.getTotalOverlapScore = function () {
return this.totalOverlapScore;
};
/**
* Returns the ring count of the current molecule.
*
* @returns {Number} The ring count.
*/
Drawer.prototype.getRingCount = function () {
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.
*/
Drawer.prototype.hasBridgedRing = function () {
return this.bridgedRing;
};
/**
* Returns the number of heavy atoms (non-hydrogen) in the current molecule.
*
* @returns {Number} The heavy atom count.
*/
Drawer.prototype.getHeavyAtomCount = function () {
var 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.
*/
Drawer.prototype.getMolecularFormula = function () {
var molecularFormula = '';
var counts = new Map();
// Initialize element count
for (var i = 0; i < this.graph.vertices.length; i++) {
var 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) {
var nHydrogens = Atom_1.default.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')) {
var count = counts.get('C');
molecularFormula += 'C' + (count > 1 ? count : '');
counts.delete('C');
}
if (counts.has('H')) {
var count = counts.get('H');
molecularFormula += 'H' + (count > 1 ? count : '');
counts.delete('H');
}
var elements = Object.keys(Atom_1.default.atomicNumbers).sort();
elements.map(function (e) {
if (counts.has(e)) {
var 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.
*/
Drawer.prototype.getRingbondType = function (vertexA, vertexB) {
var _a;
// 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 (((_a = vertexA.value.ringbonds[i]) === null || _a === void 0 ? void 0 : _a.bondType) === '-') {
return vertexB.value.ringbonds[j].bond;
}
else {
return vertexA.value.ringbonds[i].bond;
}
}
}
}
return null;
};
Drawer.prototype.initDraw = function (data, themeName, infoOnly) {
this.data = data;
this.infoOnly = infoOnly;
this.ringIdCounter = 0;
this.ringConnectionIdCounter = 0;
this.graph = new Graph_1.default(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();
};
Drawer.prototype.processGraph = function () {
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();
var 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++) {
var edge = this.graph.edges[i];
if (this.isEdgeRotatable(edge)) {
var subTreeDepthA = this.graph.getTreeDepth(edge.sourceId, edge.targetId);
var subTreeDepthB = this.graph.getTreeDepth(edge.targetId, edge.sourceId);
// Only rotate the shorter subtree
var a = edge.targetId;
var b = edge.sourceId;
if (subTreeDepthA > subTreeDepthB) {
a = edge.sourceId;
b = edge.targetId;
}
var subTreeOverlap = this.getSubtreeOverlapScore(b, a, overlapScore.vertexScores);
if (subTreeOverlap.value > this.opts.overlapSensitivity) {
var vertexA = this.graph.vertices[a];
var vertexB = this.graph.vertices[b];
var neighboursB = vertexB.getNeighbours(a);
if (neighboursB.length === 1) {
var neighbour = this.graph.vertices[neighboursB[0]];
var angle = neighbour.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper_1.default.toRad(120));
this.rotateSubtree(neighbour.id, vertexB.id, angle, vertexB.position);
// If the new overlap is bigger, undo change
var 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;
}
var neighbourA = this.graph.vertices[neighboursB[0]];
var 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 {
var angleA = neighbourA.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper_1.default.toRad(120));
var angleB = neighbourB.position.getRotateAwayFromAngle(vertexA.position, vertexB.position, MathHelper_1.default.toRad(120));
this.rotateSubtree(neighbourA.id, vertexB.id, angleA, vertexB.position);
this.rotateSubtree(neighbourB.id, vertexB.id, angleB, vertexB.position);
var 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();
}
else {
if (this.opts.atomVisualization === 'default') {
this.initPseudoElements();
}
}
this.rotateDrawing();
};
/**
* Initializes rings and ringbonds for the current molecule.
*/
Drawer.prototype.initRings = function () {
var openBonds = new Map();
// Close the open ring bonds (spanning tree -> graph)
for (var i = this.graph.vertices.length - 1; i >= 0; i--) {
var vertex = this.graph.vertices[i];
if (vertex.value.ringbonds.length === 0) {
continue;
}
for (var j = 0; j < vertex.value.ringbonds.length; j++) {
var ringbondId = vertex.value.ringbonds[j].id;
var 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 {
var sourceVertexId = vertex.id;
var targetVertexId = openBonds.get(ringbondId)[0];
var targetRingbondBond = openBonds.get(ringbondId)[1];
var edge = new Edge_1.default(sourceVertexId, targetVertexId, 1);
edge.setBondType(targetRingbondBond || ringbondBond || '-');
var edgeId = this.graph.addEdge(edge);
var 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)
var rings = SSSR_1.default.getRings(this.graph, this.opts.experimentalSSSR);
if (rings === null) {
return;
}
for (var i = 0; i < rings.length; i++) {
var ringVertices = tslib_1.__spreadArray([], tslib_1.__read(rings[i]), false);
var ringId = this.addRing(new Ring_1.default(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++) {
var a = this.rings[i];
var b = this.rings[j];
var ringConnection = new RingConnection_1.default(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++) {
var ring = this.rings[i];
ring.neighbours = RingConnection_1.default.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++) {
var 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) {
var id = -1;
for (var i = 0; i < this.rings.length; i++) {
var ring_1 = this.rings[i];
if (this.isPartOfBridgedRing(ring_1.id) && !ring_1.isBridged) {
id = ring_1.id;
}
}
if (id === -1) {
break;
}
var ring = this.getRing(id);
var 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]);
}
}
};
Drawer.prototype.initHydrogens = function () {
// 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++) {
var 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
var 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.
*/
Drawer.prototype.getBridgedRingRings = function (ringId) {
var involvedRings = Array();
var that = this;
var recurse = function (r) {
var ring = that.getRing(r);
involvedRings.push(r);
for (var i = 0; i < ring.neighbours.length; i++) {
var n = ring.neighbours[i];
if (involvedRings.indexOf(n) === -1 &&
n !== r &&
RingConnection_1.default.isBridge(that.ringConnections, that.graph.vertices, r, n)) {
recurse(n);
}
}
};
recurse(ringId);
return ArrayHelper_1.default.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.
*/
Drawer.prototype.isPartOfBridgedRing = function (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.
*/
Drawer.prototype.createBridgedRing = function (ringIds, sourceVertexId) {
var e_1, _a, e_2, _b, e_3, _c, e_4, _d;
var ringMembers = new Set();
var vertices = new Set();
var neighbours = new Set();
for (var i = 0; i < ringIds.length; i++) {
var ring_2 = this.getRing(ringIds[i]);
ring_2.isPartOfBridged = true;
for (var j = 0; j < ring_2.members.length; j++) {
vertices.add(ring_2.members[j]);
}
for (var j = 0; j < ring_2.neighbours.length; j++) {
var id = ring_2.neighbours[j];
if (ringIds.indexOf(id) === -1) {
neighbours.add(ring_2.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).
var leftovers = new Set();
try {
for (var vertices_1 = tslib_1.__values(vertices), vertices_1_1 = vertices_1.next(); !vertices_1_1.done; vertices_1_1 = vertices_1.next()) {
var id = vertices_1_1.value;
var vertex = this.graph.vertices[id];
var intersection = ArrayHelper_1.default.intersection(ringIds, vertex.value.rings);
if (vertex.value.rings.length === 1 || intersection.length === 1) {
ringMembers.add(vertex.id);
}
else {
leftovers.add(vertex.id);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (vertices_1_1 && !vertices_1_1.done && (_a = vertices_1.return)) _a.call(vertices_1);
}
finally { if (e_1) throw e_1.error; }
}
// 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();
var insideRing = Array();
try {
for (var leftovers_1 = tslib_1.__values(leftovers), leftovers_1_1 = leftovers_1.next(); !leftovers_1_1.done; leftovers_1_1 = leftovers_1.next()) {
var id = leftovers_1_1.value;
var vertex = this.graph.vertices[id];
var onRing = false;
for (var j_1 = 0; j_1 < vertex.edges.length; j_1++) {
if (this.edgeRingCount(vertex.edges[j_1]) === 1) {
onRing = true;
}
}
if (onRing) {
vertex.value.isBridgeNode = true;
ringMembers.add(vertex.id);
}
else {
vertex.value.isBridge = true;
ringMembers.add(vertex.id);
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (leftovers_1_1 && !leftovers_1_1.done && (_b = leftovers_1.return)) _b.call(leftovers_1);
}
finally { if (e_2) throw e_2.error; }
}
// Create the ring
var ring = new Ring_1.default(tslib_1.__spreadArray([], tslib_1.__read(ringMembers), false));
this.addRing(ring);
ring.isBridged = true;
ring.neighbours = tslib_1.__spreadArray([], tslib_1.__read(neighbours), false);
for (var i = 0; i < ringIds.length; i++) {
ring.rings.push(this.getRing(ringIds[i]).clone());
}
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++) {
var vertex = this.graph.vertices[insideRing[i]];
vertex.value.rings = Array();
}
try {
// Remove former rings from members of the bridged ring and add the bridged ring
for (var ringMembers_1 = tslib_1.__values(ringMembers), ringMembers_1_1 = ringMembers_1.next(); !ringMembers_1_1.done; ringMembers_1_1 = ringMembers_1.next()) {
var id = ringMembers_1_1.value;
var vertex = this.graph.vertices[id];
vertex.value.rings = ArrayHelper_1.default.removeAll(vertex.value.rings, ringIds);
vertex.value.rings.push(ring.id);
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (ringMembers_1_1 && !ringMembers_1_1.done && (_c = ringMembers_1.return)) _c.call(ringMembers_1);
}
finally { if (e_3) throw e_3.error; }
}
// 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]);
}
}
try {
// Update the ring connections and add this ring to the neighbours neighbours
for (var neighbours_1 = tslib_1.__values(neighbours), neighbours_1_1 = neighbours_1.next(); !neighbours_1_1.done; neighbours_1_1 = neighbours_1.next()) {
var id = neighbours_1_1.value;
var 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);
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (neighbours_1_1 && !neighbours_1_1.done && (_d = neighbours_1.return)) _d.call(neighbours_1);
}
finally { if (e_4) throw e_4.error; }
}
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.
*/
Drawer.prototype.areVerticesInSameRing = function (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.
*/
Drawer.prototype.getCommonRings = function (vertexA, vertexB) {
var 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.
*/
Drawer.prototype.getLargestOrAromaticCommonRing = function (vertexA, vertexB) {
var commonRings = this.getCommonRings(vertexA, vertexB);
var maxSize = 0;
var largestCommonRing = null;
if (commonRings.length === 1) {
return this.getRing(commonRings[0]);
}
for (var i = 0; i < commonRings.length; i++) {
var ring = this.getRing(commonRings[i]);
var size = ring.getSize();
// if (ring.elements.indexOf('S') !== -1) {
// return ring;
// }
if (ring.isBenzeneLike(this.graph.vertices)) {
return ring;
}
else {
if (size > maxSize) {
//NEED TO FIX
// if (( ring.edges.length < 1) //&& !ring.isHaveElements
// // && maxSize > 0
// ) {
// continue;
// }
if (maxSize > 0) {
if (largestCommonRing.members.length === 5
&& largestCommonRing.elements.indexOf('S') !== -1
&& largestCommonRing.neighbours.length === 1
&& ring.edges.length < 1) {
continue;
}
if (largestCommonRing.members.length === 6
&& largestCommonRing.edges.length < 1 && ring.edges.length < 1
&& largestCommonRing.hasDoubleBondWithO) {
continue;
}
}
maxSize = size;
largestCommonRing = ring;
}
else {
if (size === maxSize
// && size === 6
) {
if (largestCommonRing.elements.indexOf('O') !== -1) {
largestCommonRing = ring;
continue;
}
if (largestCommonRing.hasDoubleBondWithO && !ring.hasDoubleBondWithO) {
largestCommonRing = ring;
}
else {
if (!ring.hasDoubleBondWithO &&
size === 6 &&
largestCommonRing.edges.length < 1 && ring.edges.length > 1) {
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.
*/
Drawer.prototype.getVerticesAt = function (position, radius, excludeVertexId) {
var locals = Array();
for (var i = 0; i < this.graph.vertices.length; i++) {
var vertex = this.graph.vertices[i];
if (vertex.id === excludeVertexId || !vertex.positioned) {
continue;
}
var 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.
*/
Drawer.prototype.getClosestVertex = function (vertex) {
var minDist = 99999;
var minVertex = null;
for (var i = 0; i < this.graph.vertices.length; i++) {
var v = this.graph.vertices[i];
if (v.id === vertex.id) {
continue;
}
var 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.
*/
Drawer.prototype.addRing = function (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.
*/
Drawer.prototype.removeRing = function (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++) {
var 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.
*/
Drawer.prototype.getRing = function (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.
*/
Drawer.prototype.addRingConnection = function (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.
*/
Drawer.prototype.removeRingConnection = function (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.
*/
Drawer.prototype.removeRingConnectionsBetween = function (vertexIdA, vertexIdB) {
var toRemove = Array();
for (var i = 0; i < this.ringConnections.length; i++) {
var 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.
*/
Drawer.prototype.getRingConnection = function (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.
*/
Drawer.prototype.getRingConnections = function (ringId, ringIds) {
var ringConnections = Array();
for (var i = 0; i < this.ringConnections.length; i++) {
var rc = this.ringConnections[i];
for (var j = 0; j < ringIds.length; j++) {
var 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 }, ... ] }
*/
Drawer.prototype.getOverlapScore = function () {
var total = 0.0;
var 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) {
var a = this.graph.vertices[i];
var b = this.graph.vertices[j];
if (!a.value.isDrawn || !b.value.isDrawn) {
continue;
}
var dist = Vector2_1.default.subtract(a.position, b.position).lengthSq();
if (dist < this.opts.bondLengthSq) {
var weighted = (this.opts.bondLength - Math.sqrt(dist)) / this.opts.bondLength;
total += weighted;
overlapScores[i] += weighted;
overlapScores[j] += weighted;
}
}
}
var 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[]} s