/** * Authors: Frederik Leyvraz, David Degenhardt * License: GNU General Public License v3.0 only * Version: 1.0.0 */ package ch.bfh.ti.latexindexer; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; public class Plotter { private static int fileNameCounter; private final int DEFAULT_NUMBER_OF_WORDS = 20; private List words; /** * Constructor * @param words A list of the words parsed from a document. */ public Plotter(List words) { fileNameCounter = 1; this.words = new LinkedList<>(words); } /** * Creates a list of words according to the filters and rules specified by the user. * @param args An array containing the arguments passed to this command by the user. * @return The list of words as a single formatted string. */ public String print(UI.Argument[] args) { List list = getSortedList(args); int maxLength = 0; for (int i = 0; i < list.size(); i++) { maxLength = Math.max(list.get(i).getValue().length(), maxLength); } StringBuilder distribution = new StringBuilder(); for (Word word : list) { distribution.append(String.format("%-" + (maxLength + 10) + "s %d%n", word.getValue(), word.getFrequency()).replace(' ', '.')); } return distribution.toString(); } /** * Generates a plot of the words specified by the user * @param workingDirectory The working directory path where the program is being run. * @throws IOException If generatePlotTex() throws it. * @throws InterruptedException If renderPlot() throws it. */ public void generatePlot(String workingDirectory, UI.Argument[] args) throws IOException, InterruptedException { String filePath = workingDirectory + "/"; String fileName = null; for (int i = 0; i < args.length; i++){ if (args[i].getName().equals("-f")){ fileName = args[i].getValue(); args[i] = new UI.Argument(null, null); } } fileName = fileName == null ? "word-frequency-plot-" + fileNameCounter++ : fileName; filePath += fileName + ".tex"; List list = getSortedList(args); generatePlotTex(filePath, list); renderPlot(workingDirectory, fileName); } /** * Generates the .tex file to create a plot using the PGFplots package. * @param filePath The full qualified path to the .tex file that will contain the code to generate the plot. * @throws IOException If the file cannot be written to. */ private void generatePlotTex(String filePath, List list) throws IOException { try (FileWriter writer = new FileWriter(filePath)) { writer.write("\\documentclass{standalone}\n"); writer.write("\\usepackage{pgfplots}\n"); writer.write("\\pgfplotsset{compat=1.18}\n"); writer.write("\\begin{document}\n"); writer.write("\\begin{tikzpicture}\n"); writer.write("\\begin{axis}[ybar,\n"); writer.write(" bar width=1,\n"); writer.write(" ymin=0,\n"); writer.write(" xtick=data,\n"); writer.write(" xtick pos=left,\n"); writer.write(" xticklabel style={rotate=90},"); writer.write(" nodes near coords,\n"); writer.write(" xticklabels={"); for (int i = 0; i < list.size() - 1; i++) { writer.write(list.get(i).getValue() + ","); } writer.write(list.get(list.size()-1).getValue()); writer.write("},\n"); writer.write(" xlabel={Words},\n"); writer.write(" ylabel={Frequency},\n"); writer.write(" title={Frequency of parsed words},\n"); writer.write(" ymajorgrids=true,\n"); writer.write(" grid style=dashed]\n"); writer.write("\\addplot [\n"); writer.write(" color=red,\n"); writer.write(" fill=red!30\n"); writer.write(" ]\n"); writer.write(" coordinates {\n"); // Write data points for (int i = 0; i < list.size(); i++) { writer.write(" (" + i + "," + list.get(i).getFrequency() + ")\n"); } writer.write("};\n"); writer.write("\\end{axis}\n"); writer.write("\\end{tikzpicture}\n"); writer.write("\\end{document}\n"); } } /** * Calls pdflatex on the .tex file containing the PGFplots code. * @param fileName The full qualified path to the .tex file containing the PGFplots code. * @throws IOException If the file cannot be read from. * @throws InterruptedException If pdflatex cannot be run. */ private void renderPlot(String workingDirectory, String fileName) throws IOException, InterruptedException { ProcessBuilder pb = new ProcessBuilder("pdflatex", fileName); pb.directory(new File(workingDirectory)); Process process = pb.start(); int exitCode = process.waitFor(); if (exitCode == 0) { System.out.println("Successfully compiled LaTeX to PDF."); } else { System.err.println("LaTeX compilation failed."); } } /** * A helper method returning a sorted and filtered list of words according to the user's specifications. * @param args An array of arguments passed to the command by the user. * @return A list of sorted and filtered words. */ private List getSortedList(UI.Argument[] args){ List list = new LinkedList<>(this.words); int n = Math.min(DEFAULT_NUMBER_OF_WORDS, list.size()); Comparator c = new Word.FrequencyComparator(); boolean reverse = false; for (int i = 0; i < args.length; i++) { switch (args[i].getName()) { case "-n" -> { try { int m = Integer.parseInt(args[i].getValue()); if (m < 0) { System.out.println(UI.WARNING + "'-n' must be followed by a positive integer." + UI.RESET); } else { n = Math.min(m, list.size()); } } catch (NumberFormatException e) { System.out.println(UI.WARNING + "'-n' must be followed by a number." + UI.RESET); } } case "-c" -> { if (args[i].getValue().equalsIgnoreCase("a")) { c = new Word.AlphabeticalComparator(); } else if (!args[i].getValue().equalsIgnoreCase("f")) { System.out.println(UI.WARNING + "'-c' must be followed by either a or f" + UI.RESET); } } case "-p" -> { String prefix = args[i].getValue().toLowerCase(); list = list.stream().filter(word -> word.getValue().toLowerCase().startsWith(prefix)).collect(Collectors.toList()); } case "-r" -> { reverse = args[i].getValue().equalsIgnoreCase("true"); if (!args[i].getValue().equalsIgnoreCase("true") && !args[i].getValue().equalsIgnoreCase("false")){ System.out.println(UI.WARNING + "'-r' must be followed by either 'true' or 'false'" + UI.RESET); } } case null -> {} default -> System.out.println(UI.WARNING + "Dropped parameter '" + args[i].getName() + "' as it is unknown." + UI.RESET); } } list.sort(c); list = list.subList(0,Math.min(n, list.size())); if (reverse) { Collections.reverse(list); } return list; } }