content: /*************************************************************************
  *                                                                        *
  *  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.networkanalysis.GraphViz;
  
  import com.act.biointerpretation.networkanalysis.ImmutableNetwork;
  import com.act.biointerpretation.networkanalysis.NetworkEdge;
  import com.act.biointerpretation.networkanalysis.NetworkNode;
  import com.act.biointerpretation.networkanalysis.PrecursorReport;
  import com.act.jobs.FileChecker;
  import com.act.jobs.JavaRunnable;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * Class for turning a PrecursorReport into a DotGraph that can be visualized.
   * Implements JavaRunnable for workflow incorporation.
   */
  public class PrecursorReportVisualizer {
  
    private static final Logger LOGGER = LogManager.getFormatterLogger(PrecursorReportVisualizer.class);
  
    private static final Double FULL_CONFIDENCE = 1.0;
    private static final DotColor LCMS_HIT_COLOR =  DotColor.RED;
    private static final DotColor LCMS_MISS_COLOR = DotColor.DEFAULT_BLACK;
    private static final DotEdge.EdgeStyle REACTION_EDGE_STYLE = DotEdge.EdgeStyle.DEFAULT_SOLID;
    private static final DotEdge.EdgeStyle PREDICTION_EDGE_STYLE = DotEdge.EdgeStyle.DOTTED;
  
    // A map from organisms of interest to their respective colors.
    // Any edge with any organism name which contains one of these strings will be colored.
    // An edge which matches multiple keys of this map will be given one edge per distinct color specified by its
    // org matches.
    private final Map<String, DotColor> orgToColor;
  
    public void addOrgOfInterest(String org, DotColor color) {
      orgToColor.put(org, color);
    }
  
    public PrecursorReportVisualizer() {
      this.orgToColor = new HashMap<>();
    }
  
    public Runner getRunner(File inputNetwork, File outputFile) {
      return new Runner(inputNetwork, outputFile);
    }
  
    /**
     * Builds DOT graph representation of the precursor report.  The graph is printed out with the target metabolite
     * on the bottom, all its direct precursors one level up, all their precursors two levels up, etc.  Only edges
     * between adjacent levels are drawn, resulting in a reverse BFS tree representation of the precursors.
     * @param report The PrecursorReport.
     * @return The DotGraph.
     */
    public DotGraph buildDotGraph(PrecursorReport report) {
      ImmutableNetwork network = report.getNetwork();
      DotGraph graph = new DotGraph();
  
      for (NetworkNode node : network.getNodes()) {
        if (report.isPrecursor(node) && report.getLcmsConfidence(node) != null &&
            report.getLcmsConfidence(node).equals(FULL_CONFIDENCE)) {
          graph.addNode(new DotNode(node.getUID().toString(), LCMS_HIT_COLOR));
        } else {
          graph.addNode(new DotNode(node.getUID().toString(), LCMS_MISS_COLOR));
        }
      }
  
      // Add edges to the graph.  One or more DotEdges are added for each (substrate, product) pair where the substrate
      // is one level farther back in the tree than the product.
      for (NetworkEdge edge : network.getEdges()) {
        for (Integer substrate : edge.getSubstrates()) {
          for (Integer product : edge.getProducts()) {
            if (report.edgeInBfsTree(network.getNodeByUID(substrate), network.getNodeByUID(product))) {
              addEdgesToGraph(edge, substrate.toString(), product.toString(), graph);
            }
          }
        }
      }
      return graph;
    }
  
    /**
     * Helper method to build DotEdges given a NetworkEdge, and a particular (substrate, product) pair.
     * Each edge is formatted as a solid line if it has a DB reaction associated, or a dotted line otherwise.
     * The color of the edge is determined by whether it matches an organism of interest.
     * If the NetworkEdge matches multiple orgs of interest, one edge is drawn for each one it matches,
     * in the appropriate color.
     */
    private void addEdgesToGraph(NetworkEdge edge, String substrateId, String productId, DotGraph graph) {
      DotEdge.EdgeStyle style = edge.getReactionIds().isEmpty() ? PREDICTION_EDGE_STYLE : REACTION_EDGE_STYLE;
  
      Set<DotColor> colors = new HashSet<>();
      for (String orgOfInterest : orgToColor.keySet()) {
        if (matchesOrg(edge, orgOfInterest)) {
          colors.add(orgToColor.get(orgOfInterest));
        }
      }
      if (colors.isEmpty()) {
        colors.add(DotColor.DEFAULT_BLACK);
      }
  
      colors.stream().map(c -> new DotEdge(substrateId, productId, c, style)).forEach(graph::addEdge);
    }
  
    /**
     * Helper method to test whether a given edge matches the orgOfInterest string.
     * Returns true if any organism entry in the edge contains orgOfInterest as a substring.
     * For example, we can put in "homo sapiens" if we only want to match to the specific genus and species,
     * but we can also put in only "homo" and this method will return true on any organism which has "homo"
     * in its name.
     */
    private boolean matchesOrg(NetworkEdge edge, String orgOfInterest) {
      return edge.getOrgs().stream().filter(s -> s.contains(orgOfInterest)).count() > 0;
    }
  
    /**
     * Workflow-compatible component for graph visualization.
     */
    public class Runner implements JavaRunnable {
  
      private final File inputFile;
      private final File outputFile;
  
      public Runner(File inputFile, File outputFile) {
        this.inputFile = inputFile;
        this.outputFile = outputFile;
      }
  
      /**
       * Loads in a precursorReport from file, builds a DotGraph from it, and writes it to file.
       * The graph can be visualized with an online GraphViz viewer like http://www.webgraphviz.com/.
       * @throws IOException
       */
      @Override
      public void run() throws IOException {
        FileChecker.verifyInputFile(inputFile);
        FileChecker.verifyAndCreateOutputFile(outputFile);
  
        PrecursorReport report = PrecursorReport.readFromJsonFile(inputFile);
  
        LOGGER.info("Handled input files. Building dot graph.");
        DotGraph graph = buildDotGraph(report);
  
        LOGGER.info("Build graph. Writing out graph.");
        graph.writeGraphToFile(outputFile);
        LOGGER.info("Graph written to %s. Visualization complete!", outputFile.getAbsolutePath());
      }
    }
  }
  