const SvgDrawer = require('./SvgDrawer');
const SvgWrapper = require('./SvgWrapper');
const Options = require('./Options');
const ThemeManager = require('./ThemeManager');
const formulaToCommonName = require('./FormulaToCommonName');
class ReactionDrawer {
/**
* The constructor for the class ReactionDrawer.
*
* @param {Object} options An object containing reaction drawing specitic options.
* @param {Object} moleculeOptions An object containing molecule drawing specific options.
*/
constructor(options, moleculeOptions) {
this.defaultOptions = {
scale: moleculeOptions.scale > 0.0 ? moleculeOptions.scale : 1.0,
fontSize: moleculeOptions.fontSizeLarge * 0.8,
fontFamily: 'Arial, Helvetica, sans-serif',
spacing: 10,
plus: {
size: 9,
thickness: 1.0
},
arrow: {
length: moleculeOptions.bondLength * 4.0,
headSize: 6.0,
thickness: 1.0,
margin: 3
},
weights: {
normalize: false
}
}
this.opts = Options.extend(true, this.defaultOptions, options);
this.drawer = new SvgDrawer(moleculeOptions);
this.molOpts = this.drawer.opts;
}
/**
* Draws the parsed reaction smiles data to a canvas element.
*
* @param {Object} reaction The reaction object returned by the reaction smiles parser.
* @param {(String|SVGElement)} 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 {?Object} weights=null The weights for reactants, agents, and products.
* @param {String} textAbove='{reagents}' The text above the arrow.
* @param {String} textBelow='' The text below the arrow.
* @param {?Object} weights=null The weights for reactants, agents, and products.
* @param {Boolean} infoOnly=false Only output info on the molecule without drawing anything to the canvas.
*
* @returns {SVGElement} The svg element
*/
draw(reaction, target, themeName = 'light', weights = null, textAbove = '{reagents}', textBelow = '', infoOnly = false) {
this.themeManager = new ThemeManager(this.molOpts.themes, themeName);
// Normalize the weights over the reaction molecules
if (this.opts.weights.normalize) {
let max = -Number.MAX_SAFE_INTEGER;
let min = Number.MAX_SAFE_INTEGER;
if (weights.hasOwnProperty('reactants')) {
for (let i = 0; i < weights.reactants.length; i++) {
for (let j = 0; j < weights.reactants[i].length; j++) {
if (weights.reactants[i][j] < min) {
min = weights.reactants[i][j];
}
if (weights.reactants[i][j] > max) {
max = weights.reactants[i][j];
}
}
}
}
if (weights.hasOwnProperty('reagents')) {
for (let i = 0; i < weights.reagents.length; i++) {
for (let j = 0; j < weights.reagents[i].length; j++) {
if (weights.reagents[i][j] < min) {
min = weights.reagents[i][j];
}
if (weights.reagents[i][j] > max) {
max = weights.reagents[i][j];
}
}
}
}
if (weights.hasOwnProperty('products')) {
for (let i = 0; i < weights.products.length; i++) {
for (let j = 0; j < weights.products[i].length; j++) {
if (weights.products[i][j] < min) {
min = weights.products[i][j];
}
if (weights.products[i][j] > max) {
max = weights.products[i][j];
}
}
}
}
let abs_max = Math.max(Math.abs(min), Math.abs(max));
if (abs_max === 0.0) {
abs_max = 1;
}
if (weights.hasOwnProperty('reactants')) {
for (let i = 0; i < weights.reactants.length; i++) {
for (let j = 0; j < weights.reactants[i].length; j++) {
weights.reactants[i][j] /= abs_max;
}
}
}
if (weights.hasOwnProperty('reagents')) {
for (let i = 0; i < weights.reagents.length; i++) {
for (let j = 0; j < weights.reagents[i].length; j++) {
weights.reagents[i][j] /= abs_max;
}
}
}
if (weights.hasOwnProperty('products')) {
for (let i = 0; i < weights.products.length; i++) {
for (let j = 0; j < weights.products[i].length; j++) {
weights.products[i][j] /= abs_max;
}
}
}
}
let svg = null;
if (target === null || target === 'svg') {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttributeNS(null, 'width', 500 + '');
svg.setAttributeNS(null, 'height', 500 + '');
} else if (typeof target === 'string' || target instanceof String) {
svg = document.getElementById(target);
} else {
svg = target;
}
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
let elements = [];
let maxHeight = 0.0
// Reactants
for (var i = 0; i < reaction.reactants.length; i++) {
if (i > 0) {
elements.push({
width: this.opts.plus.size * this.opts.scale,
height: this.opts.plus.size * this.opts.scale,
svg: this.getPlus()
});
}
let reactantWeights = null;
if (weights && weights.hasOwnProperty('reactants') && weights.reactants.length > i) {
reactantWeights = weights.reactants[i];
}
let reactantSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.drawer.draw(reaction.reactants[i], reactantSvg, themeName, reactantWeights, infoOnly, [], this.opts.weights.normalize);
let element = {
width: reactantSvg.viewBox.baseVal.width * this.opts.scale,
height: reactantSvg.viewBox.baseVal.height * this.opts.scale,
svg: reactantSvg
};
elements.push(element);
if (element.height > maxHeight) {
maxHeight = element.height;
}
}
// Arrow
elements.push({
width: this.opts.arrow.length * this.opts.scale,
height: this.opts.arrow.headSize * 2.0 * this.opts.scale,
svg: this.getArrow()
});
// Text above the arrow / reagents
let reagentsText = "";
for (var i = 0; i < reaction.reagents.length; i++) {
if (i > 0) {
reagentsText += ", "
}
let text = this.drawer.getMolecularFormula(reaction.reagents[i]);
if (text in formulaToCommonName) {
text = formulaToCommonName[text];
}
reagentsText += SvgWrapper.replaceNumbersWithSubscript(text);
}
textAbove = textAbove.replace('{reagents}', reagentsText);
const topText = SvgWrapper.writeText(
textAbove,
this.themeManager,
this.opts.fontSize * this.opts.scale,
this.opts.fontFamily,
this.opts.arrow.length * this.opts.scale
);
let centerOffsetX = (this.opts.arrow.length * this.opts.scale - topText.width) / 2.0;
elements.push({
svg: topText.svg,
height: topText.height,
width: this.opts.arrow.length * this.opts.scale,
offsetX: -(this.opts.arrow.length * this.opts.scale + this.opts.spacing) + centerOffsetX,
offsetY: -(topText.height / 2.0) - this.opts.arrow.margin,
position: 'relative'
});
// Text below arrow
const bottomText = SvgWrapper.writeText(
textBelow,
this.themeManager,
this.opts.fontSize * this.opts.scale,
this.opts.fontFamily,
this.opts.arrow.length * this.opts.scale
);
centerOffsetX = (this.opts.arrow.length * this.opts.scale - bottomText.width) / 2.0;
elements.push({
svg: bottomText.svg,
height: bottomText.height,
width: this.opts.arrow.length * this.opts.scale,
offsetX: -(this.opts.arrow.length * this.opts.scale + this.opts.spacing) + centerOffsetX,
offsetY: bottomText.height / 2.0 + this.opts.arrow.margin,
position: 'relative'
});
// Products
for (var i = 0; i < reaction.products.length; i++) {
if (i > 0) {
elements.push({
width: this.opts.plus.size * this.opts.scale,
height: this.opts.plus.size * this.opts.scale,
svg: this.getPlus()
});
}
let productWeights = null;
if (weights && weights.hasOwnProperty('products') && weights.products.length > i) {
productWeights = weights.products[i];
}
let productSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.drawer.draw(reaction.products[i], productSvg, themeName, productWeights, infoOnly, [], this.opts.weights.normalize);
let element = {
width: productSvg.viewBox.baseVal.width * this.opts.scale,
height: productSvg.viewBox.baseVal.height * this.opts.scale,
svg: productSvg
};
elements.push(element);
if (element.height > maxHeight) {
maxHeight = element.height;
}
}
let totalWidth = 0.0;
elements.forEach(element => {
let offsetX = element.offsetX ?? 0.0;
let offsetY = element.offsetY ?? 0.0;
element.svg.setAttributeNS(null, 'x', Math.round(totalWidth + offsetX));
element.svg.setAttributeNS(null, 'y', Math.round(((maxHeight - element.height) / 2.0) + offsetY));
element.svg.setAttributeNS(null, 'width', Math.round(element.width));
element.svg.setAttributeNS(null, 'height', Math.round(element.height));
svg.appendChild(element.svg);
if (element.position !== 'relative') {
totalWidth += Math.round(element.width + this.opts.spacing + offsetX);
}
});
svg.setAttributeNS(null, 'viewBox', `0 0 ${totalWidth} ${maxHeight}`);
svg.style.width = totalWidth + 'px';
svg.style.height = maxHeight + 'px';
return svg;
}
getPlus() {
let s = this.opts.plus.size;
let w = this.opts.plus.thickness;
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
let rect_h = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
let rect_v = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
svg.setAttributeNS(null, 'id', 'plus');
rect_h.setAttributeNS(null, 'x', 0);
rect_h.setAttributeNS(null, 'y', s / 2.0 - w / 2.0);
rect_h.setAttributeNS(null, 'width', s);
rect_h.setAttributeNS(null, 'height', w);
rect_h.setAttributeNS(null, 'fill', this.themeManager.getColor("C"));
rect_v.setAttributeNS(null, 'x', s / 2.0 - w / 2.0);
rect_v.setAttributeNS(null, 'y', 0);
rect_v.setAttributeNS(null, 'width', w);
rect_v.setAttributeNS(null, 'height', s);
rect_v.setAttributeNS(null, 'fill', this.themeManager.getColor("C"));
svg.appendChild(rect_h);
svg.appendChild(rect_v);
svg.setAttributeNS(null, 'viewBox', `0 0 ${s} ${s}`);
return svg;
}
getArrowhead() {
let s = this.opts.arrow.headSize;
let marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
let polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
marker.setAttributeNS(null, 'id', 'arrowhead');
marker.setAttributeNS(null, 'viewBox', `0 0 ${s} ${s}`);
marker.setAttributeNS(null, 'markerUnits', 'userSpaceOnUse');
marker.setAttributeNS(null, 'markerWidth', s);
marker.setAttributeNS(null, 'markerHeight', s);
marker.setAttributeNS(null, 'refX', 0);
marker.setAttributeNS(null, 'refY', s / 2);
marker.setAttributeNS(null, 'orient', 'auto');
marker.setAttributeNS(null, 'fill', this.themeManager.getColor("C"));
polygon.setAttributeNS(null, 'points', `0 0, ${s} ${s / 2}, 0 ${s}`)
marker.appendChild(polygon);
return marker;
}
getCDArrowhead() {
let s = this.opts.arrow.headSize;
let sw = s * (7 / 4.5);
let marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
marker.setAttributeNS(null, 'id', 'arrowhead');
marker.setAttributeNS(null, 'viewBox', `0 0 ${sw} ${s}`);
marker.setAttributeNS(null, 'markerUnits', 'userSpaceOnUse');
marker.setAttributeNS(null, 'markerWidth', sw * 2);
marker.setAttributeNS(null, 'markerHeight', s * 2);
marker.setAttributeNS(null, 'refX', 2.2);
marker.setAttributeNS(null, 'refY', 2.2);
marker.setAttributeNS(null, 'orient', 'auto');
marker.setAttributeNS(null, 'fill', this.themeManager.getColor("C"));
path.setAttributeNS(null, 'style', 'fill-rule:nonzero;');
path.setAttributeNS(null, 'd', 'm 0 0 l 7 2.25 l -7 2.25 c 0 0 0.735 -1.084 0.735 -2.28 c 0 -1.196 -0.735 -2.22 -0.735 -2.22 z');
marker.appendChild(path);
return marker;
}
getArrow() {
let s = this.opts.arrow.headSize;
let l = this.opts.arrow.length;
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
let defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
defs.appendChild(this.getCDArrowhead());
svg.appendChild(defs);
svg.setAttributeNS(null, 'id', 'arrow');
line.setAttributeNS(null, 'x1', 0.0);
line.setAttributeNS(null, 'y1', -this.opts.arrow.thickness / 2.0);
line.setAttributeNS(null, 'x2', l);
line.setAttributeNS(null, 'y2', -this.opts.arrow.thickness / 2.0);
line.setAttributeNS(null, 'stroke-width', this.opts.arrow.thickness);
line.setAttributeNS(null, 'stroke', this.themeManager.getColor("C"));
line.setAttributeNS(null, 'marker-end', 'url(#arrowhead)');
svg.appendChild(line);
svg.setAttributeNS(null, 'viewBox', `0 ${-s / 2.0} ${l + s * (7 / 4.5)} ${s}`);
return svg;
}
}
module.exports = ReactionDrawer;