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
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.