/*************************************************************************
* *
* This file is part of the 20n/act project. *
* 20n/act enables DNA prediction for synthetic biology/bioengineering. *
* Copyright (C) 2017 20n Labs, Inc. *
* *
* Please direct all queries to act@20n.com. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*************************************************************************/
package com.act.biointerpretation.mechanisminspection;
import act.server.MongoDB;
import act.server.NoSQLAPI;
import act.shared.Chemical;
import act.shared.Reaction;
import chemaxon.calculations.clean.Cleaner;
import chemaxon.formats.MolExporter;
import chemaxon.formats.MolFormatException;
import chemaxon.formats.MolImporter;
import chemaxon.struc.Molecule;
import chemaxon.struc.RxnMolecule;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ReactionRenderer {
private static final Logger LOGGER = LogManager.getFormatterLogger(ReactionRenderer.class);
public static final String OPTION_READ_DB = "d";
public static final String OPTION_RXN_IDS = "r";
public static final String OPTION_DIR_PATH = "f";
public static final String OPTION_FILE_FORMAT = "e";
public static final String OPTION_HEIGHT = "y";
public static final String OPTION_WIDTH = "x";
public static final String OPTION_COFACTOR = "c";
public static final String HELP_MESSAGE = StringUtils.join(new String[] {
"This class renders representations of a reaction."
}, "");
public static final List<Option.Builder> OPTION_BUILDERS = new ArrayList<Option.Builder>() {{
add(Option.builder(OPTION_READ_DB)
.argName("read db name")
.desc("The name of the read DB to use")
.hasArg().required()
.longOpt("db")
);
add(Option.builder(OPTION_RXN_IDS)
.argName("id")
.desc("The id of the reaction to validate")
.hasArgs()
.valueSeparator(',')
.required()
.longOpt("id")
);
add(Option.builder(OPTION_DIR_PATH)
.argName("dir path")
.desc("The dir path where the image will be rendered")
.hasArg().required()
.longOpt("dir path")
);
// The list of file formats supported are here: https://marvin-demo.chemaxon.com/marvin/help/formats/formats.html
add(Option.builder(OPTION_FILE_FORMAT)
.argName("file format")
.desc("The file format for the image")
.hasArg().required()
.longOpt("file format")
);
add(Option.builder(OPTION_HEIGHT)
.argName("height")
.desc("height of image")
.hasArg()
.longOpt("height")
);
add(Option.builder(OPTION_WIDTH)
.argName("width")
.desc("width of image")
.hasArg()
.longOpt("width")
);
add(Option.builder(OPTION_COFACTOR)
.argName("cofactor")
.desc("true if cofactors need to be rendered, false otherwise")
.hasArg()
.longOpt("cofactor")
);
add(Option.builder("h")
.argName("help")
.desc("Prints this help message")
.longOpt("help")
);
}};
public static final HelpFormatter HELP_FORMATTER = new HelpFormatter();
static {
HELP_FORMATTER.setWidth(100);
}
private static final String DEFAULT_FORMAT = "png";
private static final Integer DEFAULT_WIDTH = 1000;
private static final Integer DEFAULT_HEIGHT = 1000;
private static final String XENON_INCHI = "InChI=1S/Xe";
private static final String SMARTS_FORMAT = "smarts";
String format;
Integer width;
Integer height;
public ReactionRenderer() {
this.format = DEFAULT_FORMAT;
this.width = DEFAULT_WIDTH;
this.height = DEFAULT_HEIGHT;
}
public ReactionRenderer(String format, Integer width, Integer height) {
this.format = format;
this.width = width;
this.height = height;
}
public void drawReaction(MongoDB db, Long reactionId, String dirPath, boolean includeCofactors) throws IOException {
RxnMolecule renderedReactionMolecule = getRxnMolecule(db, reactionId, includeCofactors);
String fileName = StringUtils.join(new String[] {reactionId.toString(), ".", format});
drawRxnMolecule(renderedReactionMolecule, new File(dirPath, fileName));
}
public String getSmartsForReaction(MongoDB db, Long reactionId, boolean includeCofactors) throws IOException {
RxnMolecule rxnMolecule = getRxnMolecule(db, reactionId, includeCofactors);
return getSmartsNotation(rxnMolecule);
}
public RxnMolecule getRxnMolecule(MongoDB db, Long reactionId, boolean includeCofactors) {
RxnMolecule renderedReactionMolecule = new RxnMolecule();
List<Long> substrateIds = getSubstrates(db, reactionId, includeCofactors);
List<Long> productIds = getProducts(db, reactionId, includeCofactors);
for (Long sub : substrateIds) {
Chemical chemical = db.getChemicalFromChemicalUUID(sub);
Molecule mol = importMoleculeOrXenon(chemical);
renderedReactionMolecule.addComponent(mol, RxnMolecule.REACTANTS);
}
for (Long prod : productIds) {
Chemical chemical = db.getChemicalFromChemicalUUID(prod);
Molecule mol = importMoleculeOrXenon(chemical);
renderedReactionMolecule.addComponent(mol, RxnMolecule.PRODUCTS);
}
return renderedReactionMolecule;
}
private List<Long> getSubstrates(MongoDB db, Long reactionId, boolean includeCofactors) {
Reaction r = db.getReactionFromUUID(reactionId);
List<Long> substrates = new ArrayList<>();
for (Long id : r.getSubstrates()) {
substrates.add(id);
}
if (includeCofactors) {
for (Long id : r.getSubstrateCofactors()) {
substrates.add(id);
}
}
return substrates;
}
private List<Long> getProducts(MongoDB db, Long reactionId, boolean includeCofactors) {
Reaction r = db.getReactionFromUUID(reactionId);
List<Long> products = new ArrayList<>();
for (Long id : r.getProducts()) {
products.add(id);
}
if (includeCofactors) {
for (Long id : r.getProductCofactors()) {
products.add(id);
}
}
return products;
}
public void drawRxnMolecule(RxnMolecule molecule, File imageFile) throws IOException {
// Change the reaction arrow type.
molecule.setReactionArrowType(RxnMolecule.REGULAR_SINGLE);
drawMolecule(molecule, imageFile);
}
public void drawMolecule(Molecule molecule, File imageFile)
throws IOException {
// Calculate coordinates with a 2D coordinate system.
Cleaner.clean(molecule, 2, null);
byte[] graphics = MolExporter.exportToBinFormat(molecule, getFormatAndSizeString());
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
fos.write(graphics);
}
}
public String getSmartsNotation(Molecule mol) throws IOException {
return MolExporter.exportToFormat(mol, SMARTS_FORMAT);
}
/**
* Imports the molecule if possible, or else returns a Xenon atom as a placeholder for rendering.
*
* @throws MolFormatException
*/
private Molecule importMoleculeOrXenon(Chemical chemical) {
try {
return chemical.importAsMolecule();
} catch (MolFormatException e) {
LOGGER.warn("No molecule returned for chemical %d. Replacing with Xenon.", chemical.getUuid());
try {
return MolImporter.importMol(XENON_INCHI);
} catch (MolFormatException f) {
LOGGER.error("Could not import xenon inchi; something is very wrong.");
throw new RuntimeException(f);
}
}
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public Integer getWidth() {
return width;
}
public void setWidth(Integer width) {
this.width = width;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
private String getFormatAndSizeString() {
return format + StringUtils.join(":w", width.toString(), ",", "h", height.toString());
}
public static void main(String[] args) throws IOException {
Options opts = new Options();
for (Option.Builder b : OPTION_BUILDERS) {
opts.addOption(b.build());
}
CommandLine cl = null;
try {
CommandLineParser parser = new DefaultParser();
cl = parser.parse(opts, args);
} catch (ParseException e) {
System.err.format("Argument parsing failed: %s\n", e.getMessage());
HELP_FORMATTER.printHelp(ReactionRenderer.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
System.exit(1);
}
if (cl.hasOption("help")) {
HELP_FORMATTER.printHelp(ReactionRenderer.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
return;
}
Integer height = Integer.parseInt(cl.getOptionValue(OPTION_HEIGHT, "1000"));
Integer width = Integer.parseInt(cl.getOptionValue(OPTION_WIDTH, "1000"));
Boolean representCofactors = cl.hasOption(OPTION_COFACTOR) && Boolean.parseBoolean(cl.getOptionValue(OPTION_COFACTOR));
NoSQLAPI api = new NoSQLAPI(cl.getOptionValue(OPTION_READ_DB), cl.getOptionValue(OPTION_READ_DB));
for (String val : cl.getOptionValues(OPTION_RXN_IDS)) {
Long reactionId = Long.parseLong(val);
ReactionRenderer renderer = new ReactionRenderer(cl.getOptionValue(OPTION_FILE_FORMAT), width, height);
renderer.drawReaction(api.getReadDB(), reactionId, cl.getOptionValue(OPTION_DIR_PATH), representCofactors);
LOGGER.info(renderer.getSmartsForReaction(api.getReadDB(), reactionId, representCofactors));
}
}
}