0
\$\begingroup\$

Intro

I have this repository.

Given this command

java -jar TriangleWavePlotter-1.6.jar --amplitude=2 --period=4 --shift=-1 --xRangeStart=-12 --xRangeEnd=12 --yRangeStart=-3 --yRangeEnd=3 %USERPROFILE%/OneDrive/Desktop/TriangleWave.png

the program will produce

Triangle wave

Code

com.github.coderodde.gnuplot.trianglewave.TriangleWavePlotter.java:

package com.github.coderodde.gnuplot.trianglewave;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class implements a triangle wave plotter.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 17, 2023)
 * @since 1.6 (Sep 17, 2023)
 */
public final class TriangleWavePlotter {
    
    private static final Logger LOGGER = 
            Logger.getLogger(TriangleWavePlotter.class.getSimpleName());
    
    private static final String TRIANGULAR_WAVE_GNUPLOT_FILE_NAME = 
            "triangle-wave.plt";
    
    private static final String TEMPORARY_PLOT_SCRIPT_PREFIX = "triangle-wave-";
    private static final String TEMPORARY_PLOT_SCRIPT_SUFFIX = ".plt";
    private static final String WINDOWS_USERPROFILE = "%USERPROFILE%";
    private static final String LINUX_USERPROFILE = "~";
    
    private static final class Anchors {
        static final String PERIOD           = "{PERIOD}"          ;
        static final String AMPLITUDE        = "{AMPLITUDE}"       ;
        static final String SHIFT            = "{SHIFT}"           ;
        static final String COLOR            = "{COLOR}"           ;
        static final String LINE_WIDTH       = "{LINE_WIDTH}"      ;
        static final String X_RANGE_START    = "{X_RANGE_START}"   ;
        static final String X_RANGE_END      = "{X_RANGE_END}"     ;
        static final String Y_RANGE_START    = "{Y_RANGE_START}"   ;
        static final String Y_RANGE_END      = "{Y_RANGE_END}"     ;
        static final String PLOT_WIDTH       = "{PLOT_WIDTH}"      ;
        static final String PLOT_HEIGHT      = "{PLOT_HEIGHT}"     ;  
        static final String OUTPUT_FILE_NAME = "{OUTPUT_FILE_NAME}";
    }
    
    private static final class CommandLineArguments {
        static final String PERIOD        = "--period"     ;
        static final String AMPLITUDE     = "--amplitude"  ;
        static final String SHIFT         = "--shift"      ;
        static final String COLOR         = "--color"      ;
        static final String LINE_WIDTH    = "--lineWidth"  ;
        static final String X_RANGE_START = "--xRangeStart";
        static final String X_RANGE_END   = "--xRangeEnd"  ;
        static final String Y_RANGE_START = "--yRangeStart";
        static final String Y_RANGE_END   = "--yRangeEnd"  ;
        static final String PLOT_WIDTH    = "--plotWidth"  ;
        static final String PLOT_HEIGHT   = "--plotHeight" ;   
    }
    
    public static void main(String[] args) {
        new TriangleWavePlotter().run(args);
    }
    
    private TriangleWavePlotter() {}
    
    void run(String[] args) {
        if (args.length < 1) {
            System.out.println("No output PNG file name provided.");
            return;
        }
        
        if (args[0].equals("--help")) {
            printHelp();
            return;
        }
        
        String outputFileName = args[args.length - 1];
        String homeDirectory = System.getProperty("user.home");
        
        if (outputFileName.startsWith(WINDOWS_USERPROFILE)) {
            outputFileName = 
                    homeDirectory
                    + File.separator
                    + outputFileName.substring(WINDOWS_USERPROFILE.length());
        } else if (outputFileName.startsWith(LINUX_USERPROFILE)) {
            outputFileName = 
                    homeDirectory
                    + File.separator
                    + outputFileName.substring(LINUX_USERPROFILE.length());
        }
        
        String plotTemplate;
        
        try {
            plotTemplate = readTriangularWaveGnuplotFile();
        } catch (IOException ex) {
            LOGGER.log(
                    Level.SEVERE,
                    "Could not read the Gnuplot template file.", 
                    ex);
            
            System.exit(1);
            return;
        }
        
        TriangleWavePlotterConfiguration triangleWavePlotterConfiguration = 
                buildTriangleWavePlotterConfiguration(args);
        
        String gnuplotScript = 
                computeGnuplotScript(
                        plotTemplate,
                        outputFileName,
                        triangleWavePlotterConfiguration);
        
        try {
            generatePNGFile(outputFileName, gnuplotScript);
        } catch (IOException ex) {
            LOGGER.log(
                    Level.SEVERE,
                    "I/O exception while generating the plot PNG.", 
                    ex);
        } catch (InterruptedException ex) {
            LOGGER.log(
                    Level.SEVERE,
                    "Interrupted while generating the plot PNG.",
                    ex);
        }
    }
    
    private static File getTemporaryGnuplotFile() throws IOException {
        return File.createTempFile(TEMPORARY_PLOT_SCRIPT_PREFIX,
                                   TEMPORARY_PLOT_SCRIPT_SUFFIX);
    }
    
    private static void generatePNGFile(String plotPNGFileName,
                                        String gnuplotScript) 
            throws IOException, InterruptedException {
        File temporaryScriptFile = getTemporaryGnuplotFile();
        Files.write(temporaryScriptFile.toPath(), gnuplotScript.getBytes());
        
        String[] commands = { 
            "gnuplot.exe",
            temporaryScriptFile.getAbsolutePath() 
        };
        
        Process process = Runtime.getRuntime().exec(commands);
        int returnStatus = process.waitFor();
        
        if (returnStatus != 0) {
            System.err.println(
                    "Gnuplot seems to fail, return status: " + returnStatus);
        } else {
            System.out.println(
                    "Generated the plot in " + plotPNGFileName);
        }
        
        if (!temporaryScriptFile.delete()) {
            System.gc();
            temporaryScriptFile.deleteOnExit();
        }
    }
    
    private String readTriangularWaveGnuplotFile() throws IOException {
        InputStream inputStream = 
                getClass()
               .getClassLoader()
               .getResourceAsStream(TRIANGULAR_WAVE_GNUPLOT_FILE_NAME);
        
        Scanner scanner = new Scanner(inputStream);
        StringBuilder stringBuilder = new StringBuilder();
        
        while (scanner.hasNextLine()) {
            stringBuilder.append(scanner.nextLine()).append("\n");
        }
        
        return stringBuilder.toString();
    }
    
    private TriangleWavePlotterConfiguration 
        buildTriangleWavePlotterConfiguration(String[] args) {
        
        TriangleWavePlotterConfiguration triangleWavePlotterConfiguration = 
                new TriangleWavePlotterConfiguration();
        
        int index = 0;
        
        for (String argument : args) {
            if (index < args.length - 1) {
                processArgument(triangleWavePlotterConfiguration, argument);
            }
                
            index++;
        }
        
        return triangleWavePlotterConfiguration;
    }
        
    private void processArgument(
            TriangleWavePlotterConfiguration triangleWavePlotterConfiguration,
            String argument) {
    
        String command = argument.split("=")[0];
        String param   = argument.split("=")[1];
            
        if (command.startsWith(CommandLineArguments.PERIOD)) {
            triangleWavePlotterConfiguration
                    .setPeriod(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.AMPLITUDE)) {
            triangleWavePlotterConfiguration
                    .setAmplitude(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.SHIFT)) {
            triangleWavePlotterConfiguration.setShift(
                    Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.COLOR)) {
            triangleWavePlotterConfiguration.setColor(param);
        } else if (command.startsWith(CommandLineArguments.LINE_WIDTH)) {
            triangleWavePlotterConfiguration
                    .setLineWidth(Integer.parseInt(param));
        } else if (command.startsWith(CommandLineArguments.X_RANGE_START)) {
            triangleWavePlotterConfiguration
                    .setXRangeStart(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.X_RANGE_END)) {
            triangleWavePlotterConfiguration
                    .setXRangeEnd(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.Y_RANGE_START)) {
            triangleWavePlotterConfiguration
                    .setYRangeStart(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.Y_RANGE_END)) {
            triangleWavePlotterConfiguration
                    .setYRangeEnd(Double.parseDouble(param));
        } else if (command.startsWith(CommandLineArguments.PLOT_WIDTH)) {
            triangleWavePlotterConfiguration.setPlotWidth(param);
        } else if (command.startsWith(CommandLineArguments.PLOT_HEIGHT)) {
            triangleWavePlotterConfiguration.setPlotHeight(param);
        } else {
            throw new IllegalStateException(
                    "Unknown command = '" + command + "'");
        }
    }
        
    private String computeGnuplotScript(
            String plotTemplate,
            String outputFileName,
            TriangleWavePlotterConfiguration triangleWavePlotterConfiguration) {
        
        return plotTemplate
                .replace(
                        Anchors.PERIOD, 
                        Double.toString(
                                triangleWavePlotterConfiguration.getPeriod()))
                .replace(
                        Anchors.AMPLITUDE,
                        Double.toString(
                                triangleWavePlotterConfiguration
                                        .getAmplitude()))
                .replace(
                        Anchors.SHIFT,
                        Double.toString(
                                triangleWavePlotterConfiguration.getShift()))
                .replace(
                        Anchors.COLOR, 
                        triangleWavePlotterConfiguration.getColor())
                .replace(
                        Anchors.LINE_WIDTH, 
                        Integer.toString(
                                triangleWavePlotterConfiguration
                                        .getLineWidth())) 
                .replace(
                        Anchors.X_RANGE_START, 
                        Double.toString(
                                triangleWavePlotterConfiguration
                                        .getXRangeStart()))
                .replace(
                        Anchors.X_RANGE_END, 
                        Double.toString(
                                triangleWavePlotterConfiguration
                                        .getXRangeEnd()))
                .replace(
                        Anchors.Y_RANGE_START,
                        Double.toString(
                                triangleWavePlotterConfiguration
                                        .getYRangeStart()))
                .replace(
                        Anchors.Y_RANGE_END, 
                        Double.toString(
                                triangleWavePlotterConfiguration
                                        .getYRangeEnd()))
                .replace(
                        Anchors.PLOT_WIDTH, 
                        triangleWavePlotterConfiguration.getPlotWidth())
                .replace(
                        Anchors.PLOT_HEIGHT, 
                        triangleWavePlotterConfiguration.getPlotHeight())
                .replace(Anchors.OUTPUT_FILE_NAME, outputFileName);
    }
    
    private void printHelp() {
        String jarFileName;
        
        try {
            jarFileName = getJarFileName();
        } catch (URISyntaxException ex) {
            LOGGER.log(Level.SEVERE, "Could not get the JAR file name.", ex);
            System.exit(1);
            return;
        }
        
        System.out.println(
        """
        java -jar %s [OPTIONS] OUTPUT_FILE_NAME
        Where OPTIONS can have:
            --period=PERIOD       sets the period, positive floating-point-value
            --amplitude=AMPLITUDE sets the amplitude, positive floating-point value
            --shift               sets the shift, floating-point value
            --color               sets the line color, long or short RGB value, or predefined color name
            --lineWidth           sets the line width, positive integer
            --xRangeStart         sets the X-range start, floating-point value
            --xRangeEnd           sets the X-range end, floating-point value
            --yRangeStart         sets the Y-range start, floating-point value
            --yRangeEnd           sets the Y-range end, floating-point value
            --plotWidth           sets the plot width, string
            --plotHeight          sets the plot height, string
        """.formatted(jarFileName));
    }
    
    private String getJarFileName() throws URISyntaxException {
        String absolutePath = getClass()
          .getProtectionDomain()
          .getCodeSource()
          .getLocation()
          .toURI()
          .getPath();
        
        String[] pathComponents = absolutePath.split("/");
        return pathComponents[pathComponents.length - 1];
    }
}

com.github.coderodde.gnuplot.trianglewave.TriangleWavePlotterConfiguration.java:

package com.github.coderodde.gnuplot.trianglewave;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class holds all the configuration parameters for plotting a 
 * triangle wave.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 17, 2023)
 * @since 1.6 (Sep 17, 2023)
 */
public final class TriangleWavePlotterConfiguration {
    
    private static final Pattern SHORT_COLOR_RGB_HEX = 
            Pattern.compile("#[0-9A-F]{3}", Pattern.CASE_INSENSITIVE);
    
    private static final class Defaults {
        static final double PERIOD        = 2.0 * Math.PI ;
        static final double AMPLITUDE     = 1.0           ;
        static final double SHIFT         = 0.0           ;
        static final String COLOR         = "#ffd500"     ;
        static final int LINE_WIDTH       = 8             ;
        static final double X_RANGE_START = -4.0 * Math.PI;
        static final double X_RANGE_END   =  4.0 * Math.PI;
        static final double Y_RANGE_START = -1.5          ;
        static final double Y_RANGE_END   = 1.5           ;
        static final String PLOT_WIDTH    = "960"         ;
        static final String PLOT_HEIGHT   = "600"         ;
    }
    
    private double period      = Defaults.PERIOD       ;      
    private double amplitude   = Defaults.AMPLITUDE    ;
    private double shift       = Defaults.SHIFT        ;
    private String color       = Defaults.COLOR        ;
    private int lineWidth      = Defaults.LINE_WIDTH   ;
    private double xRangeStart = Defaults.X_RANGE_START;
    private double xRangeEnd   = Defaults.X_RANGE_END  ;
    private double yRangeStart = Defaults.Y_RANGE_START;
    private double yRangeEnd   = Defaults.Y_RANGE_END  ;
    private String plotWidth   = Defaults.PLOT_WIDTH   ;
    private String plotHeight  = Defaults.PLOT_HEIGHT  ; 

    public double getPeriod() {
        return period;
    }
    
    public void setPeriod(double period) {
        this.period = requireDoubleIsPositive(period);
    }
    
    public double getAmplitude() {
        return amplitude;
    }
    
    public void setAmplitude(double amplitude) {
        this.amplitude = requireDoubleIsPositive(amplitude);
    }
    
    public double getShift() {
        return shift;
    }
    
    public void setShift(double shift) {
        double actualShift = shift % (2.0 * Math.PI);
        
        if (actualShift < 0.0) {
            actualShift += 2.0 * Math.PI;
        }
        
        this.shift = actualShift;
    }
    
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = validateHexColor(color);
    }

    public int getLineWidth() {
        return lineWidth;
    }

    public void setLineWidth(int lineWidth) {
        if (lineWidth < 1) {
            throw new IllegalArgumentException(
                    "Line width is too small: " 
                            + lineWidth 
                            + ". Must be at least 1.");
        }
        
        this.lineWidth = lineWidth;
    }

    public double getXRangeStart() {
        return xRangeStart;
    }

    public void setXRangeStart(double xRangeStart) {
        this.xRangeStart = requireFiniteDouble(xRangeStart);
    }

    public double getXRangeEnd() {
        return xRangeEnd;
    }

    public void setXRangeEnd(double xRangeEnd) {
        this.xRangeEnd = requireFiniteDouble(xRangeEnd);
    }

    public double getYRangeStart() {
        return yRangeStart;
    }

    public void setYRangeStart(double yRangeStart) {
        this.yRangeStart = requireFiniteDouble(yRangeStart);
    }

    public double getYRangeEnd() {
        return yRangeEnd;
    }

    public void setYRangeEnd(double yRangeEnd) {
        this.yRangeEnd = requireFiniteDouble(yRangeEnd);
    }

    public String getPlotWidth() {
        return plotWidth;
    }

    public void setPlotWidth(String plotWidth) {
        this.plotWidth = plotWidth;
    }

    public String getPlotHeight() {
        return plotHeight;
    }

    public void setPlotHeight(String plotHeight) {
        this.plotHeight = plotHeight;
    }
    
    private static double requireFiniteDouble(double d) {
        if (Double.isNaN(d)) {
            throw new IllegalArgumentException("The input double is NaN.");
        }
        
        if (Double.isInfinite(d)) {
            throw new IllegalArgumentException(
                    "The input double is inifinite in absolute value.");
        }
        
        return d;
    }
    
    private static double requireDoubleIsPositive(double d) {
        if (Double.isNaN(d)) {
            throw new IllegalArgumentException("The input double is NaN.");
        }
        
        if (d <= 0.0) {
            throw new IllegalArgumentException(
                    "The input double is not positive.");
        }
        
        if (Double.isInfinite(d)) {
            throw new IllegalArgumentException("The input double is infinite.");
        }
        
        return d;
    }
    
    static String validateHexColor(String hexColorCandidate) {
        Matcher matcherShoftRGBHex = 
                SHORT_COLOR_RGB_HEX.matcher(hexColorCandidate);
        
        if (matcherShoftRGBHex.matches()) {
            return convertShortRGBToLong(hexColorCandidate);
        }
        
        // Perhaps, it's a color name. Just return and wish for the good.
        return hexColorCandidate;
    }
    
    private static String convertShortRGBToLong(String hexColor) {
        char r = hexColor.charAt(1);
        char g = hexColor.charAt(2);
        char b = hexColor.charAt(3);
        
        return new StringBuilder()
                .append("#")
                .append(r)
                .append(r)
                .append(g)
                .append(g)
                .append(b)
                .append(b)
                .toString();
    }
}

triangle-wave.plt:

p = {PERIOD}
a = {AMPLITUDE}
s = {SHIFT}

set key noautotitle
set style line 1 linecolor rgb '{COLOR}' linewidth {LINE_WIDTH}

set xrange [{X_RANGE_START}:{X_RANGE_END}]
set yrange [{Y_RANGE_START}:{Y_RANGE_END}]

# set xtics 0, 1 format "%d"

set terminal pngcairo enhanced size {PLOT_WIDTH},{PLOT_HEIGHT}
set output '{OUTPUT_FILE_NAME}'

f(x) = (4 * a / p) * (x - s - (p / 2) * floor(2 * (x - s) / p + 0.5)) * (-1) ** floor(2 * (x - s) / p + 0.5)
plot f(x) with lines ls 1

replot
exit

Critique request

As always, please tell me whatever comes to mind.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.