diff --git a/apps/jrobin/java/src/org/rrd4j/core/Archive.java b/apps/jrobin/java/src/org/rrd4j/core/Archive.java index 36ce2e801..64ba9163c 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/Archive.java +++ b/apps/jrobin/java/src/org/rrd4j/core/Archive.java @@ -352,7 +352,7 @@ public class Archive implements RrdUpdater { long startTime = getStartTime(); for (int i = 0; i < rows.get(); i++) { long time = startTime + i * getArcStep(); - writer.writeComment(Util.getDate(time) + " / " + time); + writer.writeComment(writer.formatTimestamp(time) + " / " + time); writer.startTag("row"); for (Robin robin : robins) { writer.writeTag("v", robin.getValue(i)); diff --git a/apps/jrobin/java/src/org/rrd4j/core/FetchData.java b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java index f00938631..8a44052bd 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/FetchData.java +++ b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java @@ -406,39 +406,43 @@ public class FetchData { public void exportXml(OutputStream outputStream) { //No auto flush for XmlWriter, it will be flushed once, when export is finished try (XmlWriter writer = new XmlWriter(outputStream, false)) { - writer.startTag("fetch_data"); - writer.startTag("request"); - writer.writeTag("file", request.getParentDb().getPath()); - writer.writeComment(Util.getDate(request.getFetchStart())); - writer.writeTag("start", request.getFetchStart()); - writer.writeComment(Util.getDate(request.getFetchEnd())); - writer.writeTag("end", request.getFetchEnd()); - writer.writeTag("resolution", request.getResolution()); - writer.writeTag("cf", request.getConsolFun()); - writer.closeTag(); // request - writer.startTag("datasources"); - for (String dsName : dsNames) { - writer.writeTag("name", dsName); - } - writer.closeTag(); // datasources - writer.startTag("data"); - for (int i = 0; i < timestamps.length; i++) { - writer.startTag("row"); - writer.writeComment(Util.getDate(timestamps[i])); - writer.writeTag("timestamp", timestamps[i]); - writer.startTag("values"); - for (int j = 0; j < dsNames.length; j++) { - writer.writeTag("v", values[j][i]); - } - writer.closeTag(); // values - writer.closeTag(); // row - } - writer.closeTag(); // data - writer.closeTag(); // fetch_data - writer.flush(); + exportXml(writer); } } + public void exportXml(XmlWriter writer) { + writer.startTag("fetch_data"); + writer.startTag("request"); + writer.writeTag("file", request.getParentDb().getPath()); + writer.writeComment(request.getFetchStart()); + writer.writeTag("start", request.getFetchStart()); + writer.writeComment(request.getFetchEnd()); + writer.writeTag("end", request.getFetchEnd()); + writer.writeTag("resolution", request.getResolution()); + writer.writeTag("cf", request.getConsolFun()); + writer.closeTag(); // request + writer.startTag("datasources"); + for (String dsName : dsNames) { + writer.writeTag("name", dsName); + } + writer.closeTag(); // datasources + writer.startTag("data"); + for (int i = 0; i < timestamps.length; i++) { + writer.startTag("row"); + writer.writeComment(timestamps[i]); + writer.writeTag("timestamp", timestamps[i]); + writer.startTag("values"); + for (int j = 0; j < dsNames.length; j++) { + writer.writeTag("v", values[j][i]); + } + writer.closeTag(); // values + writer.closeTag(); // row + } + writer.closeTag(); // data + writer.closeTag(); // fetch_data + writer.flush(); + } + /** * Dumps fetch data to file in XML format. * diff --git a/apps/jrobin/java/src/org/rrd4j/core/Header.java b/apps/jrobin/java/src/org/rrd4j/core/Header.java index 69710d33f..294db5f95 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/Header.java +++ b/apps/jrobin/java/src/org/rrd4j/core/Header.java @@ -173,7 +173,7 @@ public class Header implements RrdUpdater
{ writer.writeTag("version", RRDTOOL_VERSION3); writer.writeComment("Seconds"); writer.writeTag("step", step.get()); - writer.writeComment(Util.getDate(lastUpdateTime.get())); + writer.writeComment(lastUpdateTime.get()); writer.writeTag("lastupdate", lastUpdateTime.get()); } diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java index 75491729b..097c9a3a3 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java @@ -214,7 +214,7 @@ public abstract class RrdBackendFactory implements Closeable { /** * Return the current active factories as a stream. - * @return + * @return the Stream * @since 3.7 */ public static synchronized Stream getActiveFactories() { diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java index 073dc8c04..e4e9c5633 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java @@ -212,7 +212,7 @@ public class RrdDb implements RrdUpdater, Closeable { /** * Internal method used to memorize the pool, without generating a loop * @param pool - * @return + * @return the Builder */ Builder setPoolInternal(RrdDbPool pool) { this.pool = pool; @@ -1150,15 +1150,14 @@ public class RrdDb implements RrdUpdater, Closeable { } /** - * Writes the RRD content to OutputStream using XML format. This format + * Writes the RRD content to {@link XmlWriter} using XML format. This format * is fully compatible with RRDTool's XML dump format and can be used for conversion * purposes or debugging. * - * @param destination Output stream to receive XML data + * @param writer {@link XmlWriter} to receive XML data * @throws java.io.IOException Thrown in case of I/O related error */ - public synchronized void dumpXml(OutputStream destination) throws IOException { - XmlWriter writer = new XmlWriter(destination); + public synchronized void dumpXml(XmlWriter writer) throws IOException { writer.startTag("rrd"); // dump header header.appendXml(writer); @@ -1174,6 +1173,18 @@ public class RrdDb implements RrdUpdater, Closeable { writer.flush(); } + /** + * Writes the RRD content to {@link OutputStream} using XML format. This format + * is fully compatible with RRDTool's XML dump format and can be used for conversion + * purposes or debugging. + * + * @param destination Output stream to receive XML data + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized void dumpXml(OutputStream destination) throws IOException { + dumpXml(new XmlWriter(destination)); + } + /** * This method is just an alias for {@link #dumpXml(OutputStream) dumpXml} method. * @@ -1193,9 +1204,10 @@ public class RrdDb implements RrdUpdater, Closeable { * @throws java.io.IOException Thrown in case of I/O related error */ public synchronized String getXml() throws IOException { - ByteArrayOutputStream destination = new ByteArrayOutputStream(XML_BUFFER_CAPACITY); - dumpXml(destination); - return destination.toString(); + try (ByteArrayOutputStream destination = new ByteArrayOutputStream(XML_BUFFER_CAPACITY)) { + dumpXml(destination); + return destination.toString(); + } } /** diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java index b8afea589..37674ab99 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java @@ -633,38 +633,52 @@ public class RrdDef { */ public void exportXmlTemplate(OutputStream out, boolean compatible) { try (XmlWriter xml = new XmlWriter(out)) { - xml.startTag("rrd_def"); - if (compatible) { - xml.writeTag("path", getPath()); - } else { - xml.writeTag("uri", getUri()); - } - xml.writeTag("step", getStep()); - xml.writeTag("start", getStartTime()); - // datasources - DsDef[] dsDefs = getDsDefs(); - for (DsDef dsDef : dsDefs) { - xml.startTag("datasource"); - xml.writeTag("name", dsDef.getDsName()); - xml.writeTag("type", dsDef.getDsType()); - xml.writeTag("heartbeat", dsDef.getHeartbeat()); - xml.writeTag("min", dsDef.getMinValue(), "U"); - xml.writeTag("max", dsDef.getMaxValue(), "U"); - xml.closeTag(); // datasource - } - ArcDef[] arcDefs = getArcDefs(); - for (ArcDef arcDef : arcDefs) { - xml.startTag("archive"); - xml.writeTag("cf", arcDef.getConsolFun()); - xml.writeTag("xff", arcDef.getXff()); - xml.writeTag("steps", arcDef.getSteps()); - xml.writeTag("rows", arcDef.getRows()); - xml.closeTag(); // archive - } - xml.closeTag(); // rrd_def + exportXmlTemplate(xml, compatible); } } + /** + * Exports RrdDef object to output stream in XML format. Generated XML code can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class. + *

If compatible is set to true, it returns an XML compatible with previous RRD4J's versions, using + * a path, instead of an URI.

+ * + * @param xml XML writer + * @param compatible Compatible with previous versions. + */ + public void exportXmlTemplate(XmlWriter xml, boolean compatible) { + xml.startTag("rrd_def"); + if (compatible) { + xml.writeTag("path", getPath()); + } else { + xml.writeTag("uri", getUri()); + } + xml.writeTag("step", getStep()); + xml.writeTag("start", getStartTime()); + // datasources + DsDef[] dsDefs = getDsDefs(); + for (DsDef dsDef : dsDefs) { + xml.startTag("datasource"); + xml.writeTag("name", dsDef.getDsName()); + xml.writeTag("type", dsDef.getDsType()); + xml.writeTag("heartbeat", dsDef.getHeartbeat()); + xml.writeTag("min", dsDef.getMinValue(), "U"); + xml.writeTag("max", dsDef.getMaxValue(), "U"); + xml.closeTag(); // datasource + } + ArcDef[] arcDefs = getArcDefs(); + for (ArcDef arcDef : arcDefs) { + xml.startTag("archive"); + xml.writeTag("cf", arcDef.getConsolFun()); + xml.writeTag("xff", arcDef.getXff()); + xml.writeTag("steps", arcDef.getSteps()); + xml.writeTag("rows", arcDef.getRows()); + xml.closeTag(); // archive + } + xml.closeTag(); // rrd_def + xml.flush(); + } + /** *

Exports RrdDef object to string in XML format. Generated XML string can be parsed * with {@link org.rrd4j.core.RrdDefTemplate} class.

diff --git a/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java index 3b71535d4..abe59775f 100644 --- a/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java +++ b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java @@ -1,16 +1,16 @@ package org.rrd4j.core; -import java.awt.Color; -import java.awt.Font; +import java.awt.*; import java.io.File; import java.io.OutputStream; import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Deque; import java.util.LinkedList; import java.util.Locale; -import java.util.TimeZone; /** * Extremely simple utility class used to create XML documents. @@ -18,22 +18,37 @@ import java.util.TimeZone; public class XmlWriter implements AutoCloseable { static final String INDENT_STR = " "; private static final String STYLE = "style"; - private static final SimpleDateFormat ISOLIKE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ENGLISH); - static { - ISOLIKE.setTimeZone(TimeZone.getTimeZone("UTC")); + private static final DateTimeFormatter ISOLIKE = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ") + .withLocale(Locale.ENGLISH) + .withZone(ZoneId.of("UTC")); + private static final String DEFAULT_NAN_STRING = Double.toString(Double.NaN); + + @FunctionalInterface + public interface DoubleFormater { + String format(double value, String nanString); } private final PrintWriter writer; private final StringBuilder indent = new StringBuilder(); private final Deque openTags = new LinkedList<>(); + private final DateTimeFormatter timeFormatter; + private final DoubleFormater doubleFormatter; + + private XmlWriter(PrintWriter writer, DateTimeFormatter timeFormatter, DoubleFormater doubleFormatter) { + this.writer = writer; + this.timeFormatter = timeFormatter; + this.doubleFormatter = doubleFormatter; + } /** - * Creates XmlWriter with the specified output stream to send XML code to. + * Creates XmlWriter with the specified {@link OutputStream} to send XML code to. * - * @param stream Output stream which receives XML code + * @param stream {@link OutputStream} which receives XML code */ public XmlWriter(OutputStream stream) { writer = new PrintWriter(stream, true); + timeFormatter = ISOLIKE; + this.doubleFormatter = (d, n) -> Util.formatDouble(d, n,true); } /** @@ -44,6 +59,44 @@ public class XmlWriter implements AutoCloseable { */ public XmlWriter(OutputStream stream, boolean autoFlush) { writer = new PrintWriter(stream, autoFlush); + timeFormatter = ISOLIKE; + this.doubleFormatter = (d, n) -> Util.formatDouble(d, n,true); + } + + /** + * Creates XmlWriter with the specified {@link PrintWriter} to send XML code to. + * + * @param stream {@link PrintWriter} which receives XML code + */ + public XmlWriter(PrintWriter stream) { + writer = stream; + timeFormatter = ISOLIKE; + this.doubleFormatter = (d, n) -> Util.formatDouble(d, n,true); + } + + /** + * Return a new {@link XmlWriter} that will format time stamp as ISO 8601 with this explicit time zone {@link ZoneId} + * @param zid + * @return the XmlWriter + */ + public XmlWriter withTimeZone(ZoneId zid) { + if (indent.length() != 0 || !openTags.isEmpty()) { + throw new IllegalStateException("Can't be used on a already used XmlWriter"); + } + DateTimeFormatter dtf = this.timeFormatter.withZone(zid); + return new XmlWriter(writer, dtf, doubleFormatter); + } + + /** + * Return a new {@link XmlWriter} that will format time stamp using this {@link ZoneId} + * @param doubleFormatter + * @return the XmlWriter + */ + public XmlWriter withDoubleFormatter(DoubleFormater doubleFormatter) { + if (indent.length() != 0 || !openTags.isEmpty()) { + throw new IllegalStateException("Can't be used on a already used XmlWriter"); + } + return new XmlWriter(writer, timeFormatter, doubleFormatter); } /** @@ -110,7 +163,7 @@ public class XmlWriter implements AutoCloseable { * @param nanString a {@link java.lang.String} object. */ public void writeTag(String tag, double value, String nanString) { - writeTag(tag, Util.formatDouble(value, nanString, true)); + writeTag(tag, doubleFormatter.format(value, nanString)); } /** @@ -120,7 +173,7 @@ public class XmlWriter implements AutoCloseable { * @param value value to be placed between <tag> and </tag> */ public void writeTag(String tag, double value) { - writeTag(tag, Util.formatDouble(value, true)); + writeTag(tag, doubleFormatter.format(value, DEFAULT_NAN_STRING)); } /** @@ -193,14 +246,28 @@ public class XmlWriter implements AutoCloseable { * @param comment comment string */ public void writeComment(Object comment) { - if (comment instanceof Date) { - synchronized (ISOLIKE) { - comment = ISOLIKE.format((Date) comment); - } - } writer.println(indent + ""); } + /** + * Writes a timestamp using the configured {@link DateTimeFormatter} as an XML comment to output stream + * + * @param timestamp + */ + public void writeComment(long timestamp) { + writer.println(indent + ""); + } + + /** + * Format a timestamp using the configured {@link DateTimeFormatter} + * + * @param timestamp + * @return the formatted timestamp + */ + public String formatTimestamp(long timestamp) { + return timeFormatter.format(Instant.ofEpochSecond(timestamp)); + } + private static String escape(String s) { return s.replace("<", "<").replace(">", ">"); } diff --git a/apps/jrobin/java/src/org/rrd4j/data/Source.java b/apps/jrobin/java/src/org/rrd4j/data/Source.java index cfa5050d5..179298002 100644 --- a/apps/jrobin/java/src/org/rrd4j/data/Source.java +++ b/apps/jrobin/java/src/org/rrd4j/data/Source.java @@ -33,7 +33,7 @@ abstract class Source { /** * @param tStart * @param tEnd - * @return + * @return the Aggregates * @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)} */ @Deprecated @@ -46,7 +46,7 @@ abstract class Source { * @param tStart * @param tEnd * @param percentile - * @return + * @return the percentile * @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable.PERCENTILE}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)} */ @Deprecated diff --git a/apps/jrobin/java/src/org/rrd4j/graph/FindUnit.java b/apps/jrobin/java/src/org/rrd4j/graph/FindUnit.java new file mode 100644 index 000000000..829c9b05c --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/graph/FindUnit.java @@ -0,0 +1,25 @@ +package org.rrd4j.graph; + +class FindUnit { + + private static final char UNIT_UNKNOWN = '?'; + private static final char[] UNIT_SYMBOLS = {'y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', ' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'}; + private static final int SYMBOLS_CENTER = 8; + + private FindUnit() { + + } + + public static char resolveSymbol(double digits) { + if (Double.isNaN(digits)) { + return UNIT_UNKNOWN; + } else { + if (((digits + SYMBOLS_CENTER) < UNIT_SYMBOLS.length) && ((digits + SYMBOLS_CENTER) >= 0)) { + return UNIT_SYMBOLS[(int) digits + SYMBOLS_CENTER]; + } else { + return UNIT_UNKNOWN; + } + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ImageParameters.java b/apps/jrobin/java/src/org/rrd4j/graph/ImageParameters.java index 5d7135f3c..2ec55a0ee 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/ImageParameters.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/ImageParameters.java @@ -1,5 +1,7 @@ package org.rrd4j.graph; +import java.util.function.DoubleUnaryOperator; + class ImageParameters { long start, end; double minval, maxval; @@ -18,5 +20,5 @@ class ImageParameters { int yorigin; int unitslength; int xgif, ygif; - String unit; + DoubleUnaryOperator log; } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/LegendComposer.java b/apps/jrobin/java/src/org/rrd4j/graph/LegendComposer.java index c029fac3b..1157c81ef 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/LegendComposer.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/LegendComposer.java @@ -15,16 +15,16 @@ class LegendComposer implements RrdGraphConstants { private final double smallLeading; private final double boxSpace; - LegendComposer(RrdGraph rrdGraph, int legX, int legY, int legWidth) { - this.gdef = rrdGraph.gdef; - this.worker = rrdGraph.worker; + LegendComposer(RrdGraphGenerator generator, int legX, int legY, int legWidth) { + this.gdef = generator.gdef; + this.worker = generator.worker; this.legX = legX; this.legY = legY; this.legWidth = legWidth; - interLegendSpace = rrdGraph.getInterlegendSpace(); - leading = rrdGraph.getLeading(); - smallLeading = rrdGraph.getSmallLeading(); - boxSpace = rrdGraph.getBoxSpace(); + interLegendSpace = generator.getInterlegendSpace(); + leading = generator.getLeading(); + smallLeading = generator.getSmallLeading(); + boxSpace = generator.getBoxSpace(); } int placeComments() { diff --git a/apps/jrobin/java/src/org/rrd4j/graph/LogService.java b/apps/jrobin/java/src/org/rrd4j/graph/LogService.java new file mode 100644 index 000000000..83138b11f --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/graph/LogService.java @@ -0,0 +1,57 @@ +package org.rrd4j.graph; + +import java.util.function.DoubleUnaryOperator; + +class LogService { + + static DoubleUnaryOperator resolve(ImageParameters im) { + boolean sameSign = Math.signum(im.minval) == Math.signum(im.maxval); + double absMinVal = Math.min(Math.abs(im.minval), Math.abs(im.maxval)); + double absMaxVal = Math.max(Math.abs(im.minval), Math.abs(im.maxval)); + if (! sameSign) { + return LogService::log10; + } else if (absMinVal == 0 && absMaxVal < 1) { + double correction = 1.0 / absMaxVal; + return v -> log10(v, correction, absMinVal); + } else if (absMinVal == 0) { + return LogService::log10; + } else if (absMinVal < 1) { + double correction = 1.0 / absMinVal; + return v -> log10(v, correction, absMinVal); + } else { + return LogService::log10; + } + } + + private LogService() { + + } + + /** + * Compute logarithm for the purposes of y-axis. + */ + private static double log10(double v, double correction, double minval) { + if (v == minval) { + return 0.0; + } else { + double lv = Math.log10(Math.abs(v) * correction); + if (lv < 0 || Double.isNaN(lv)) { + // Don't cross the sign line, round to 0 if that's the case + return 0.0; + } else { + return Math.copySign(lv, v); + } + } + } + + private static double log10(double v) { + double lv = Math.log10(Math.abs(v)); + if (lv < 0) { + // Don't cross the sign line, round to 0 if that's the case + return 0.0; + } else { + return Math.copySign(lv, v); + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java b/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java index e4598b827..a5c3ab442 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java @@ -5,18 +5,6 @@ class Mapper { private final ImageParameters im; private final double pixieX, pixieY; - Mapper(RrdGraph rrdGraph) { - this.gdef = rrdGraph.gdef; - this.im = rrdGraph.im; - pixieX = (double) im.xsize / (double) (im.end - im.start); - if (!gdef.logarithmic) { - pixieY = im.ysize / (im.maxval - im.minval); - } - else { - pixieY = im.ysize / (ValueAxisLogarithmic.log10(im.maxval) - ValueAxisLogarithmic.log10(im.minval)); - } - } - Mapper(RrdGraphDef gdef, ImageParameters im) { this.gdef = gdef; this.im = im; @@ -25,7 +13,7 @@ class Mapper { pixieY = im.ysize / (im.maxval - im.minval); } else { - pixieY = im.ysize / (Math.log10(im.maxval) - Math.log10(im.minval)); + pixieY = im.ysize / (im.log.applyAsDouble(im.maxval) - im.log.applyAsDouble(im.minval)); } } @@ -43,7 +31,7 @@ class Mapper { yval = im.yorigin; } else { - yval = im.yorigin - pixieY * (ValueAxisLogarithmic.log10(value) - ValueAxisLogarithmic.log10(im.minval)) + 0.5; + yval = im.yorigin - pixieY * (im.log.applyAsDouble(value) - im.log.applyAsDouble(im.minval)) + 0.5; } } if (!gdef.rigid) { diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java index 3623c8115..e34c5ec2f 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java @@ -1,51 +1,23 @@ package org.rrd4j.graph; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; import java.awt.Graphics; -import java.awt.Paint; -import java.awt.Stroke; -import java.awt.Transparency; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; +import java.util.function.Supplier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.swing.ImageIcon; -import org.rrd4j.core.Util; import org.rrd4j.data.DataProcessor; -import org.rrd4j.graph.DownSampler.DataSet; /** * Class which actually creates Rrd4j graphs (does the hard work). */ public class RrdGraph implements RrdGraphConstants { - private static final double[] SENSIBLE_VALUES = { - 1000.0, 900.0, 800.0, 750.0, 700.0, - 600.0, 500.0, 400.0, 300.0, 250.0, - 200.0, 125.0, 100.0, 90.0, 80.0, - 75.0, 70.0, 60.0, 50.0, 40.0, 30.0, - 25.0, 20.0, 10.0, 9.0, 8.0, - 7.0, 6.0, 5.0, 4.0, 3.5, 3.0, - 2.5, 2.0, 1.8, 1.5, 1.2, 1.0, - 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0, -1 - }; - - private static final int SYMBOLS_CENTER = 8; - private static final char[] SYMBOLS = {'y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', ' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'}; final RrdGraphDef gdef; final ImageParameters im; - private DataProcessor dproc; - ImageWorker worker; - Mapper mapper; - private final RrdGraphInfo info = new RrdGraphInfo(); - private final String signature; + private final RrdGraphInfo info; /** * Creates graph from the corresponding {@link org.rrd4j.graph.RrdGraphDef} object. @@ -54,19 +26,7 @@ public class RrdGraph implements RrdGraphConstants { * @throws java.io.IOException Thrown in case of I/O error */ public RrdGraph(RrdGraphDef gdef) throws IOException { - this.gdef = gdef; - signature = gdef.getSignature(); - im = new ImageParameters(); - - worker = BufferedImageWorker.getBuilder().setGdef(gdef).build(); - try { - createGraph(); - } - finally { - worker.dispose(); - worker = null; - dproc = null; - } + this(gdef, () -> RrdGraph.generateImageWorker(gdef)); } /** @@ -76,21 +36,9 @@ public class RrdGraph implements RrdGraphConstants { * @throws IOException */ public RrdGraph(RrdGraphDef gdef, ImageWorker worker) throws IOException { - this.gdef = gdef; - signature = gdef.getSignature(); - im = new ImageParameters(); - this.worker = worker; - try { - createGraph(); - } - finally { - worker.dispose(); - this.worker = null; - dproc = null; - } + this(gdef, () -> worker); } - /** *

Creates graph from the corresponding {@link org.rrd4j.graph.RrdGraphDef} object.

*

The graph will be created using customs {@link javax.imageio.ImageWriter} and {@link javax.imageio.ImageWriteParam} given.

@@ -103,18 +51,28 @@ public class RrdGraph implements RrdGraphConstants { * @since 3.5 */ public RrdGraph(RrdGraphDef gdef, ImageWriter writer, ImageWriteParam param) throws IOException { + this(gdef, () -> RrdGraph.generateImageWorker(gdef, writer, param)); + } + + private RrdGraph(RrdGraphDef gdef, Supplier worker) throws IOException { this.gdef = gdef; - signature = gdef.getSignature(); - im = new ImageParameters(); - worker = BufferedImageWorker.getBuilder().setGdef(gdef).setWriter(writer).setImageWriteParam(param).build(); + RrdGraphGenerator generator = new RrdGraphGenerator(gdef, worker.get(), new DataProcessor(gdef.startTime, gdef.endTime)); try { - createGraph(); + generator.createGraph(); } finally { - worker.dispose(); - worker = null; - dproc = null; + generator.worker.dispose(); } + info = generator.info; + im = generator.im; + } + + private static ImageWorker generateImageWorker(RrdGraphDef gdef, ImageWriter writer, ImageWriteParam param) { + return BufferedImageWorker.getBuilder().setGdef(gdef).setWriter(writer).setImageWriteParam(param).build(); + } + + private static ImageWorker generateImageWorker(RrdGraphDef gdef) { + return BufferedImageWorker.getBuilder().setGdef(gdef).build(); } /** @@ -126,704 +84,6 @@ public class RrdGraph implements RrdGraphConstants { return info; } - private void createGraph() throws IOException { - boolean lazy = lazyCheck(); - if (!lazy || gdef.printStatementCount() != 0) { - fetchData(); - resolveTextElements(); - if (gdef.shouldPlot() && !lazy) { - initializeLimits(); - calculatePlotValues(); - findMinMaxValues(); - identifySiUnit(); - expandValueRange(); - removeOutOfRangeRules(); - removeOutOfRangeSpans(); - mapper = new Mapper(this); - placeLegends(); - createImageWorker(); - drawBackground(); - drawData(); - drawGrid(); - drawAxis(); - drawText(); - drawLegend(); - drawRulesAndSpans(); - gator(); - drawOverlay(); - saveImage(); - } - } - collectInfo(); - } - - private void collectInfo() { - info.filename = gdef.filename; - info.width = im.xgif; - info.height = im.ygif; - for (CommentText comment : gdef.comments) { - if (comment instanceof PrintText) { - PrintText pt = (PrintText) comment; - if (pt.isPrint()) { - info.addPrintLine(pt.resolvedText); - } - } - } - if (gdef.imageInfo != null) { - info.imgInfo = Util.sprintf(gdef.locale, gdef.imageInfo, gdef.filename, im.xgif, im.ygif); - } - } - - private void saveImage() throws IOException { - if (! RrdGraphConstants.IN_MEMORY_IMAGE.equals(gdef.filename)) { - Path imgpath = Paths.get(gdef.filename); - worker.saveImage(gdef.filename); - info.bytesSource = () -> { - try { - return Files.readAllBytes(imgpath); - } catch (IOException e) { - throw new IllegalStateException("Unable to read image bytes", e); - } - }; - info.bytesCount = () -> { - try { - return (int) Files.size(imgpath); - } catch (IOException e) { - throw new IllegalStateException("Unable to read image informations", e); - } - }; - } - else { - byte[] content = worker.getImageBytes(); - info.bytesSource = () -> Arrays.copyOf(content, content.length); - info.bytesCount = () -> content.length; - } - } - - private void drawOverlay() throws IOException { - if (gdef.overlayImage != null) { - worker.loadImage(gdef.overlayImage, 0, 0, im.xgif, im.ygif); - } - } - - private void gator() { - if (!gdef.onlyGraph && gdef.showSignature) { - worker.setTextAntiAliasing(gdef.textAntiAliasing); - Font font = gdef.getFont(FONTTAG_WATERMARK); - int x = (int) (im.xgif - 2 - worker.getFontAscent(font)); - int y = 4; - worker.transform(x, y, Math.PI / 2); - worker.drawString(signature, 0, 0, font, Color.LIGHT_GRAY); - worker.reset(); - worker.setTextAntiAliasing(false); - } - } - - private void drawRulesAndSpans() { - boolean found = false; - for (PlotElement pe : gdef.plotElements) { - if (pe instanceof HRule) { - HRule hr = (HRule) pe; - if (hr.value >= im.minval && hr.value <= im.maxval) { - int y = mapper.ytr(hr.value); - if (!found) { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); - found = true; - } - worker.drawLine(im.xorigin, y, im.xorigin + im.xsize, y, hr.color, hr.stroke); - } - } - else if (pe instanceof VRule) { - VRule vr = (VRule) pe; - if (vr.timestamp >= im.start && vr.timestamp <= im.end) { - int x = mapper.xtr(vr.timestamp); - if (!found) { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); - found = true; - } - worker.drawLine(x, im.yorigin, x, im.yorigin - im.ysize, vr.color, vr.stroke); - } - } - else if (pe instanceof HSpan) { - HSpan hr = (HSpan) pe; - int ys = mapper.ytr(hr.start); - int ye = mapper.ytr(hr.end); - int height = ys - ye; - if (!found) { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); - found = true; - } - worker.fillRect(im.xorigin, ys - height, im.xsize, height, hr.color); - } - else if (pe instanceof VSpan) { - VSpan vr = (VSpan) pe; - int xs = mapper.xtr(vr.start); - int xe = mapper.xtr(vr.end); - if (!found) { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); - found = true; - } - worker.fillRect(xs, im.yorigin - im.ysize, xe - xs, im.ysize, vr.color); - } - } - if (found) - worker.reset(); - } - - private void drawText() { - if (!gdef.onlyGraph) { - worker.setTextAntiAliasing(gdef.textAntiAliasing); - if (gdef.title != null) { - // I2P truncate on the right only - //int x = im.xgif / 2 - (int) (worker.getStringWidth(gdef.title, gdef.getFont(FONTTAG_TITLE)) / 2); - int x = Math.max(2, im.xgif / 2 - (int) (worker.getStringWidth(gdef.title, gdef.getFont(FONTTAG_TITLE)) / 2)); - // I2P a little less padding on top and more on the bottom - //int y = PADDING_TOP + (int) worker.getFontAscent(gdef.getFont(FONTTAG_TITLE)); - int y = PADDING_TOP * 2 / 3 + (int) worker.getFontAscent(gdef.getFont(FONTTAG_TITLE)); - worker.drawString(gdef.title, x, y, gdef.getFont(FONTTAG_TITLE), gdef.getColor(ElementsNames.font)); - } - if (gdef.verticalLabel != null) { - int y = im.yorigin - im.ysize / 2 + (int) worker.getStringWidth(gdef.verticalLabel, gdef.getFont(FONTTAG_UNIT)) / 2; - int ascent = (int) worker.getFontAscent(gdef.getFont(FONTTAG_UNIT)); - worker.transform(PADDING_LEFT, y, -Math.PI / 2); - worker.drawString(gdef.verticalLabel, 0, ascent, gdef.getFont(FONTTAG_UNIT), gdef.getColor(ElementsNames.font)); - worker.reset(); - } - worker.setTextAntiAliasing(false); - } - } - - private void drawGrid() { - if (!gdef.onlyGraph) { - worker.setTextAntiAliasing(gdef.textAntiAliasing); - Paint shade1 = gdef.getColor(ElementsNames.shadea); - Paint shade2 = gdef.getColor(ElementsNames.shadeb); - Stroke borderStroke = new BasicStroke(1); - worker.drawLine(0, 0, im.xgif - 1, 0, shade1, borderStroke); - worker.drawLine(1, 1, im.xgif - 2, 1, shade1, borderStroke); - worker.drawLine(0, 0, 0, im.ygif - 1, shade1, borderStroke); - worker.drawLine(1, 1, 1, im.ygif - 2, shade1, borderStroke); - worker.drawLine(im.xgif - 1, 0, im.xgif - 1, im.ygif - 1, shade2, borderStroke); - worker.drawLine(0, im.ygif - 1, im.xgif - 1, im.ygif - 1, shade2, borderStroke); - worker.drawLine(im.xgif - 2, 1, im.xgif - 2, im.ygif - 2, shade2, borderStroke); - worker.drawLine(1, im.ygif - 2, im.xgif - 2, im.ygif - 2, shade2, borderStroke); - if (gdef.drawXGrid) { - new TimeAxis(this).draw(); - } - if (gdef.drawYGrid) { - boolean ok; - if (gdef.altYMrtg) { - ok = new ValueAxisMrtg(this).draw(); - } - else if (gdef.logarithmic) { - ok = new ValueAxisLogarithmic(this).draw(); - } - else { - ok = new ValueAxis(this).draw(); - } - if (!ok) { - String msg = "No Data Found"; - worker.drawString(msg, - im.xgif / 2 - (int) worker.getStringWidth(msg, gdef.getFont(FONTTAG_TITLE)) / 2, - (2 * im.yorigin - im.ysize) / 2, - gdef.getFont(FONTTAG_TITLE), gdef.getColor(ElementsNames.font)); - } - } - worker.setTextAntiAliasing(false); - } - } - - private void drawData() { - worker.setAntiAliasing(gdef.antiAliasing); - worker.clip(im.xorigin, im.yorigin - gdef.height - 1, gdef.width, gdef.height + 2); - double areazero = mapper.ytr((im.minval > 0.0) ? im.minval : (im.maxval < 0.0) ? im.maxval : 0.0); - double[] x = gdef.downsampler == null ? xtr(dproc.getTimestamps()) : null; - double[] lastY = null; - // draw line, area and stack - for (PlotElement plotElement : gdef.plotElements) { - if (plotElement instanceof SourcedPlotElement) { - SourcedPlotElement source = (SourcedPlotElement) plotElement; - double[] y; - if (gdef.downsampler != null) { - DataSet set = gdef.downsampler.downsize(dproc.getTimestamps(), source.getValues()); - x = xtr(set.timestamps); - y = ytr(set.values); - } else { - y = ytr(source.getValues()); - } - if (Line.class.isAssignableFrom(source.getClass())) { - worker.drawPolyline(x, y, source.color, ((Line)source).stroke ); - } - else if (Area.class.isAssignableFrom(source.getClass())) { - if(source.parent == null) { - worker.fillPolygon(x, areazero, y, source.color); - } - else { - worker.fillPolygon(x, lastY, y, source.color); - worker.drawPolyline(x, lastY, source.getParentColor(), new BasicStroke(0)); - } - } - else if (source instanceof Stack) { - Stack stack = (Stack) source; - float width = stack.getParentLineWidth(); - if (width >= 0F) { - // line - worker.drawPolyline(x, y, stack.color, new BasicStroke(width)); - } - else { - // area - worker.fillPolygon(x, lastY, y, stack.color); - worker.drawPolyline(x, lastY, stack.getParentColor(), new BasicStroke(0)); - } - } - else { - // should not be here - throw new IllegalStateException("Unknown plot source: " + source.getClass().getName()); - } - lastY = y; - } - } - worker.reset(); - worker.setAntiAliasing(false); - } - - private void drawAxis() { - if (!gdef.onlyGraph) { - Paint gridColor = gdef.getColor(ElementsNames.grid); - Paint xaxisColor = gdef.getColor(ElementsNames.xaxis); - Paint yaxisColor = gdef.getColor(ElementsNames.yaxis); - Paint arrowColor = gdef.getColor(ElementsNames.arrow); - Stroke stroke = new BasicStroke(1); - worker.drawLine(im.xorigin + im.xsize, im.yorigin, im.xorigin + im.xsize, im.yorigin - im.ysize, - gridColor, stroke); - worker.drawLine(im.xorigin, im.yorigin - im.ysize, im.xorigin + im.xsize, im.yorigin - im.ysize, - gridColor, stroke); - worker.drawLine(im.xorigin - 4, im.yorigin, im.xorigin + im.xsize + 4, im.yorigin, - xaxisColor, stroke); - worker.drawLine(im.xorigin, im.yorigin + 4, im.xorigin, im.yorigin - im.ysize - 4, - yaxisColor, stroke); - - // I2P skip arrowheads if transparent - if (((Color)arrowColor).getAlpha() == 0) - return; - - //Do X axis arrow - double[] Xarrow_x = { - im.xorigin + im.xsize + 4, - im.xorigin + im.xsize + 9, - im.xorigin + im.xsize + 4, - }; - double[] Xarrow_y = { - im.yorigin - 3, im.yorigin, - im.yorigin + 3, - }; - worker.fillPolygon(Xarrow_x, im.yorigin + 3.0, Xarrow_y, arrowColor); - - //Do y axis arrow - double[] Yarrow_x = { - im.xorigin - 3, - im.xorigin, - im.xorigin + 3, - }; - double[] Yarrow_y = { - im.yorigin - im.ysize - 4, - im.yorigin - im.ysize - 9, - im.yorigin - im.ysize - 4, - }; - worker.fillPolygon(Yarrow_x, im.yorigin - im.ysize - 4.0, Yarrow_y, arrowColor); - } - } - - private void drawBackground() throws IOException { - worker.fillRect(0, 0, im.xgif, im.ygif, gdef.getColor(ElementsNames.back)); - if (gdef.backgroundImage != null) { - worker.loadImage(gdef.backgroundImage, 0, 0, im.xgif, im.ygif); - } - if (gdef.canvasImage != null) { - worker.loadImage(gdef.canvasImage, im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize); - } - worker.fillRect(im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize, gdef.getColor(ElementsNames.canvas)); - } - - private void createImageWorker() { - worker.resize(im.xgif, im.ygif); - } - - private void placeLegends() { - if (!gdef.noLegend && !gdef.onlyGraph) { - int border = (int) (getFontCharWidth(FontTag.LEGEND) * PADDING_LEGEND); - LegendComposer lc = new LegendComposer(this, border, im.ygif, im.xgif - 2 * border); - im.ygif = lc.placeComments() + PADDING_BOTTOM; - } - } - - private void initializeLimits() { - im.xsize = gdef.width; - im.ysize = gdef.height; - im.unitslength = gdef.unitsLength; - - if (gdef.onlyGraph) { - im.xorigin = 0; - } - else { - im.xorigin = (int) (PADDING_LEFT + im.unitslength * getFontCharWidth(FONTTAG_AXIS)); - } - - if (!gdef.onlyGraph && gdef.verticalLabel != null) { - im.xorigin += getFontHeight(FONTTAG_UNIT); - } - - if (gdef.onlyGraph) { - im.yorigin = im.ysize; - } - else { - im.yorigin = PADDING_TOP + im.ysize; - } - - if (!gdef.onlyGraph && gdef.title != null) { - im.yorigin += getFontHeight(FONTTAG_TITLE) + PADDING_TITLE; - } - - if (gdef.onlyGraph) { - im.xgif = im.xsize; - im.ygif = im.yorigin; - } - else { - im.xgif = PADDING_RIGHT + im.xsize + im.xorigin; - im.ygif = im.yorigin + (int) (PADDING_PLOT * getFontHeight(FONTTAG_AXIS)); - } - } - - private void removeOutOfRangeRules() { - for (PlotElement plotElement : gdef.plotElements) { - if (plotElement instanceof HRule) { - ((HRule) plotElement).setLegendVisibility(im.minval, im.maxval, gdef.forceRulesLegend); - } - else if (plotElement instanceof VRule) { - ((VRule) plotElement).setLegendVisibility(im.start, im.end, gdef.forceRulesLegend); - } - } - } - - private void removeOutOfRangeSpans() { - for (PlotElement plotElement : gdef.plotElements) { - if (plotElement instanceof HSpan) { - ((HSpan) plotElement).setLegendVisibility(im.minval, im.maxval, gdef.forceRulesLegend); - } - else if (plotElement instanceof VSpan) { - ((VSpan) plotElement).setLegendVisibility(im.start, im.end, gdef.forceRulesLegend); - } - } - } - - private void expandValueRange() { - im.ygridstep = (gdef.valueAxisSetting != null) ? gdef.valueAxisSetting.gridStep : Double.NaN; - im.ylabfact = (gdef.valueAxisSetting != null) ? gdef.valueAxisSetting.labelFactor : 0; - if (!gdef.rigid && !gdef.logarithmic) { - double scaled_min, scaled_max, adj; - if (Double.isNaN(im.ygridstep)) { - if (gdef.altYMrtg) { /* mrtg */ - im.decimals = Math.ceil(Math.log10(Math.max(Math.abs(im.maxval), Math.abs(im.minval)))); - im.quadrant = 0; - if (im.minval < 0) { - im.quadrant = 2; - if (im.maxval <= 0) { - im.quadrant = 4; - } - } - switch (im.quadrant) { - case 2: - im.scaledstep = Math.ceil(50 * Math.pow(10, -(im.decimals)) * Math.max(Math.abs(im.maxval), - Math.abs(im.minval))) * Math.pow(10, im.decimals - 2); - scaled_min = -2 * im.scaledstep; - scaled_max = 2 * im.scaledstep; - break; - case 4: - im.scaledstep = Math.ceil(25 * Math.pow(10, - -(im.decimals)) * Math.abs(im.minval)) * Math.pow(10, im.decimals - 2); - scaled_min = -4 * im.scaledstep; - scaled_max = 0; - break; - default: /* quadrant 0 */ - im.scaledstep = Math.ceil(25 * Math.pow(10, -(im.decimals)) * im.maxval) * - Math.pow(10, im.decimals - 2); - scaled_min = 0; - scaled_max = 4 * im.scaledstep; - break; - } - im.minval = scaled_min; - im.maxval = scaled_max; - } - else if (gdef.altAutoscale || (gdef.altAutoscaleMin && gdef.altAutoscaleMax)) { - /* measure the amplitude of the function. Make sure that - graph boundaries are slightly higher then max/min vals - so we can see amplitude on the graph */ - double delt, fact; - - delt = im.maxval - im.minval; - adj = delt * 0.1; - fact = 2.0 * Math.pow(10.0, - Math.floor(Math.log10(Math.max(Math.abs(im.minval), Math.abs(im.maxval)))) - 2); - if (delt < fact) { - adj = (fact - delt) * 0.55; - } - im.minval -= adj; - im.maxval += adj; - } - else if (gdef.altAutoscaleMin) { - /* measure the amplitude of the function. Make sure that - graph boundaries are slightly lower than min vals - so we can see amplitude on the graph */ - adj = (im.maxval - im.minval) * 0.1; - im.minval -= adj; - } - else if (gdef.altAutoscaleMax) { - /* measure the amplitude of the function. Make sure that - graph boundaries are slightly higher than max vals - so we can see amplitude on the graph */ - adj = (im.maxval - im.minval) * 0.1; - im.maxval += adj; - } - else { - scaled_min = im.minval / im.magfact; - scaled_max = im.maxval / im.magfact; - for (int i = 1; SENSIBLE_VALUES[i] > 0; i++) { - if (SENSIBLE_VALUES[i - 1] >= scaled_min && SENSIBLE_VALUES[i] <= scaled_min) { - im.minval = SENSIBLE_VALUES[i] * im.magfact; - } - if (-SENSIBLE_VALUES[i - 1] <= scaled_min && -SENSIBLE_VALUES[i] >= scaled_min) { - im.minval = -SENSIBLE_VALUES[i - 1] * im.magfact; - } - if (SENSIBLE_VALUES[i - 1] >= scaled_max && SENSIBLE_VALUES[i] <= scaled_max) { - im.maxval = SENSIBLE_VALUES[i - 1] * im.magfact; - } - if (-SENSIBLE_VALUES[i - 1] <= scaled_max && -SENSIBLE_VALUES[i] >= scaled_max) { - im.maxval = -SENSIBLE_VALUES[i] * im.magfact; - } - } - } - } - else { - im.minval = im.ylabfact * im.ygridstep * - Math.floor(im.minval / (im.ylabfact * im.ygridstep)); - im.maxval = im.ylabfact * im.ygridstep * - Math.ceil(im.maxval / (im.ylabfact * im.ygridstep)); - } - - } - } - - private void identifySiUnit() { - im.unitsexponent = gdef.unitsExponent; - im.base = gdef.base; - if (!gdef.logarithmic) { - double digits; - if (im.unitsexponent != Integer.MAX_VALUE) { - digits = Math.floor(im.unitsexponent / 3.0); - } - else { - digits = Math.floor(Math.log(Math.max(Math.abs(im.minval), Math.abs(im.maxval))) / Math.log(im.base)); - } - im.magfact = Math.pow(im.base, digits); - if (((digits + SYMBOLS_CENTER) < SYMBOLS.length) && ((digits + SYMBOLS_CENTER) >= 0)) { - im.symbol = SYMBOLS[(int) digits + SYMBOLS_CENTER]; - } - else { - im.symbol = '?'; - } - } - } - - private void findMinMaxValues() { - double minval = Double.NaN, maxval = Double.NaN; - for (PlotElement pe : gdef.plotElements) { - if (pe instanceof SourcedPlotElement) { - minval = Util.min(((SourcedPlotElement) pe).getMinValue(), minval); - maxval = Util.max(((SourcedPlotElement) pe).getMaxValue(), maxval); - } - } - if (Double.isNaN(minval)) { - minval = 0D; - } - if (Double.isNaN(maxval)) { - maxval = 1D; - } - im.minval = gdef.minValue; - im.maxval = gdef.maxValue; - /* adjust min and max values */ - if (Double.isNaN(im.minval) || ((!gdef.logarithmic && !gdef.rigid) && im.minval > minval)) { - im.minval = minval; - } - if (Double.isNaN(im.maxval) || (!gdef.rigid && im.maxval < maxval)) { - if (gdef.logarithmic) { - im.maxval = maxval * 1.1; - } - else { - im.maxval = maxval; - } - } - /* make sure min is smaller than max */ - if (im.minval > im.maxval) { - im.minval = 0.99 * im.maxval; - } - /* make sure min and max are not equal */ - if (Math.abs(im.minval - im.maxval) < .0000001) { - im.maxval *= 1.01; - if (!gdef.logarithmic) { - im.minval *= 0.99; - } - /* make sure min and max are not both zero */ - if (im.maxval == 0.0) { - im.maxval = 1.0; - } - } - } - - private void calculatePlotValues() { - for (PlotElement pe : gdef.plotElements) { - if (pe instanceof SourcedPlotElement) { - ((SourcedPlotElement) pe).assignValues(dproc); - } - } - } - - private void resolveTextElements() { - ValueScaler valueScaler = new ValueScaler(gdef.base); - for (CommentText comment : gdef.comments) { - comment.resolveText(gdef.locale, dproc, valueScaler); - } - } - - private void fetchData() throws IOException { - dproc = new DataProcessor(gdef.startTime, gdef.endTime); - dproc.setPixelCount(gdef.width); - if (gdef.poolUsed) { - dproc.setPoolUsed(gdef.poolUsed); - dproc.setPool(gdef.getPool()); - } - dproc.setTimeZone(gdef.tz); - if (gdef.step > 0) { - dproc.setStep(gdef.step); - dproc.setFetchRequestResolution(gdef.step); - } - for (Source src : gdef.sources) { - src.requestData(dproc); - } - dproc.processData(); - im.start = gdef.startTime; - im.end = gdef.endTime; - } - - private boolean lazyCheck() throws IOException { - // redraw if lazy option is not set or file does not exist - if (!gdef.lazy || !Util.fileExists(gdef.filename)) { - return false; // 'false' means 'redraw' - } - // redraw if not enough time has passed - long secPerPixel = (gdef.endTime - gdef.startTime) / gdef.width; - long elapsed = Util.getTimestamp() - Util.getLastModifiedTime(gdef.filename); - return elapsed <= secPerPixel; - } - - private void drawLegend() { - if (!gdef.onlyGraph && !gdef.noLegend) { - worker.setTextAntiAliasing(gdef.textAntiAliasing); - int ascent = (int) worker.getFontAscent(gdef.getFont(FONTTAG_LEGEND)); - int box = (int) getBox(), boxSpace = (int) (getBoxSpace()); - for (CommentText c : gdef.comments) { - if (c.isValidGraphElement()) { - int x = c.x, y = c.y + ascent; - if (c instanceof LegendText) { - // draw with BOX - worker.fillRect(x, y - box, box, box, gdef.getColor(ElementsNames.frame)); - Paint bc = gdef.getColor(ElementsNames.back); - Paint lc = ((LegendText) c).legendColor; - boolean bt = bc.getTransparency() != Transparency.OPAQUE; - boolean lt = lc.getTransparency() != Transparency.OPAQUE; - // no use drawing unless both the two on top have some transparency - if (bt && lt) - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, gdef.getColor(ElementsNames.canvas)); - // no use drawing unless the one on top has some transparency - if (lt) - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, bc); - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, lc); - worker.drawString(c.resolvedText, x + boxSpace, y, gdef.getFont(FONTTAG_LEGEND), gdef.getColor(ElementsNames.font)); - } - else { - worker.drawString(c.resolvedText, x, y, gdef.getFont(FONTTAG_LEGEND), gdef.getColor(ElementsNames.font)); - } - } - } - worker.setTextAntiAliasing(false); - } - } - - // helper methods - - double getFontHeight(FontTag fonttag) { - return worker.getFontHeight(gdef.getFont(fonttag)); - } - - double getFontCharWidth(FontTag fonttag) { - return worker.getStringWidth("a", gdef.getFont(fonttag)); - } - - @Deprecated - double getSmallFontHeight() { - return getFontHeight(FONTTAG_LEGEND); - } - - double getTitleFontHeight() { - return getFontHeight(FONTTAG_TITLE); - } - - double getInterlegendSpace() { - return getFontCharWidth(FONTTAG_LEGEND) * LEGEND_INTERSPACING; - } - - double getLeading() { - return getFontHeight(FONTTAG_LEGEND) * LEGEND_LEADING; - } - - double getSmallLeading() { - return getFontHeight(FONTTAG_LEGEND) * LEGEND_LEADING_SMALL; - } - - double getBoxSpace() { - return Math.ceil(getFontHeight(FONTTAG_LEGEND) * LEGEND_BOX_SPACE); - } - - private double getBox() { - return getFontHeight(FONTTAG_LEGEND) * LEGEND_BOX; - } - - private double[] xtr(long[] timestamps) { - double[] timestampsDev = new double[2 * timestamps.length - 1]; - for (int i = 0, j = 0; i < timestamps.length; i += 1, j += 2) { - timestampsDev[j] = mapper.xtr(timestamps[i]); - if (i < timestamps.length - 1) { - timestampsDev[j + 1] = timestampsDev[j]; - } - } - return timestampsDev; - } - - private double[] ytr(double[] values) { - double[] valuesDev = new double[2 * values.length - 1]; - for (int i = 0, j = 0; i < values.length; i += 1, j += 2) { - if (Double.isNaN(values[i])) { - valuesDev[j] = Double.NaN; - } - else { - valuesDev[j] = mapper.ytr(values[i]); - } - if (j > 0) { - valuesDev[j - 1] = valuesDev[j]; - } - } - return valuesDev; - } - /** * Renders this graph onto graphing device * @@ -834,4 +94,5 @@ public class RrdGraph implements RrdGraphConstants { ImageIcon image = new ImageIcon(imageData); image.paintIcon(null, g, 0, 0); } + } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java index 298f36b44..424a3a51e 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java @@ -15,7 +15,9 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.TimeZone; +import java.util.function.Function; import javax.imageio.ImageIO; @@ -108,7 +110,8 @@ public class RrdGraphDef implements RrdGraphConstants, DataHolder { String filename = RrdGraphConstants.IN_MEMORY_IMAGE; // ok long startTime, endTime; // ok TimeAxisSetting timeAxisSetting = null; // ok - TimeLabelFormat timeLabelFormat = null; // ok + TimeLabelFormat timeLabelFormat = null; + Function> formatProvider = s -> Optional.empty(); ValueAxisSetting valueAxisSetting = null; // ok boolean altYGrid = false; // ok boolean noMinorGrid = false; // ok @@ -396,6 +399,54 @@ public class RrdGraphDef implements RrdGraphConstants, DataHolder { timeLabelFormat = format; } + /** + *

This allows to keep the default major and minor grid unit, but with changing only the label formatting, + * that will be formatted differently according to {@link TimeUnit} chosen for the time axis.

+ *

If the returned {@link Optional} is empty, the default formatting will be kept

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Default formatting
{@link TimeUnit}Default pattern
MINUTEHH:mm
HOURHH:mm
DAYEEE dd
WEEK'Week 'w
MONTHMMM
YEARyy
+ * + * + * @param formatProvider An {@link Optional} holding the {@link TimeLabelFormat} to use or empy to keep the default. + * @since 3.10 + */ + public void setTimeLabelFormatter(Function> formatProvider) { + this.formatProvider = formatProvider; + } + /** * Sets vertical axis grid and labels. Makes vertical grid lines appear * at gridStep interval. Every labelFactor*gridStep, a major grid line is printed, @@ -1939,4 +1990,11 @@ public class RrdGraphDef implements RrdGraphConstants, DataHolder { return this.step; } + /** + * @since 3.10 + */ + boolean drawTicks() { + return tickStroke != null && ((! (tickStroke instanceof BasicStroke)) || ((BasicStroke)tickStroke).getLineWidth() > 0); + } + } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphGenerator.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphGenerator.java new file mode 100644 index 000000000..cad2be9f9 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphGenerator.java @@ -0,0 +1,725 @@ +package org.rrd4j.graph; + +import java.awt.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import org.rrd4j.core.Util; +import org.rrd4j.data.DataProcessor; + +class RrdGraphGenerator { + + private static final double[] SENSIBLE_VALUES = { + 1000.0, 900.0, 800.0, 750.0, 700.0, + 600.0, 500.0, 400.0, 300.0, 250.0, + 200.0, 125.0, 100.0, 90.0, 80.0, + 75.0, 70.0, 60.0, 50.0, 40.0, 30.0, + 25.0, 20.0, 10.0, 9.0, 8.0, + 7.0, 6.0, 5.0, 4.0, 3.5, 3.0, + 2.5, 2.0, 1.8, 1.5, 1.2, 1.0, + 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0, -1 + }; + private static final int SYMBOLS_CENTER = 8; + private static final char[] SYMBOLS = {'y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', ' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'}; + + final RrdGraphDef gdef; + final ImageWorker worker; + private final DataProcessor dproc; + Mapper mapper; + final RrdGraphInfo info = new RrdGraphInfo(); + private final String signature; + final ImageParameters im; + + RrdGraphGenerator(RrdGraphDef gdef, ImageWorker worker, DataProcessor dproc) { + this.gdef = gdef; + this.worker = worker; + this.dproc = dproc; + signature = gdef.getSignature(); + im = new ImageParameters(); + } + + void createGraph() throws IOException { + boolean lazy = lazyCheck(); + if (!lazy || gdef.printStatementCount() != 0) { + fetchData(); + resolveTextElements(); + if (gdef.shouldPlot() && !lazy) { + initializeLimits(); + calculatePlotValues(); + findMinMaxValues(); + identifySiUnit(); + expandValueRange(); + removeOutOfRangeRules(); + removeOutOfRangeSpans(); + mapper = new Mapper(gdef, im); + placeLegends(); + createImageWorker(); + drawBackground(); + drawData(); + drawGrid(); + drawAxis(); + drawText(); + drawLegend(); + drawRulesAndSpans(); + gator(); + drawOverlay(); + saveImage(); + } + } + collectInfo(); + } + + private void collectInfo() { + info.filename = gdef.filename; + info.width = im.xgif; + info.height = im.ygif; + for (CommentText comment : gdef.comments) { + if (comment instanceof PrintText) { + PrintText pt = (PrintText) comment; + if (pt.isPrint()) { + info.addPrintLine(pt.resolvedText); + } + } + } + if (gdef.imageInfo != null) { + info.imgInfo = Util.sprintf(gdef.locale, gdef.imageInfo, gdef.filename, im.xgif, im.ygif); + } + } + + private void saveImage() throws IOException { + if (!RrdGraphConstants.IN_MEMORY_IMAGE.equals(gdef.filename)) { + Path imgpath = Paths.get(gdef.filename); + worker.saveImage(gdef.filename); + info.bytesSource = () -> { + try { + return Files.readAllBytes(imgpath); + } catch (IOException e) { + throw new IllegalStateException("Unable to read image bytes", e); + } + }; + info.bytesCount = () -> { + try { + return (int) Files.size(imgpath); + } catch (IOException e) { + throw new IllegalStateException("Unable to read image informations", e); + } + }; + } else { + byte[] content = worker.getImageBytes(); + info.bytesSource = () -> Arrays.copyOf(content, content.length); + info.bytesCount = () -> content.length; + } + } + + private void drawOverlay() throws IOException { + if (gdef.overlayImage != null) { + worker.loadImage(gdef.overlayImage, 0, 0, im.xgif, im.ygif); + } + } + + private void gator() { + if (!gdef.onlyGraph && gdef.showSignature) { + worker.setTextAntiAliasing(gdef.textAntiAliasing); + Font font = gdef.getFont(RrdGraphConstants.FONTTAG_WATERMARK); + int x = (int) (im.xgif - 2 - worker.getFontAscent(font)); + int y = 4; + worker.transform(x, y, Math.PI / 2); + worker.drawString(signature, 0, 0, font, Color.LIGHT_GRAY); + worker.reset(); + worker.setTextAntiAliasing(false); + } + } + + private void drawRulesAndSpans() { + boolean found = false; + for (PlotElement pe : gdef.plotElements) { + if (pe instanceof HRule) { + HRule hr = (HRule) pe; + if (hr.value >= im.minval && hr.value <= im.maxval) { + int y = mapper.ytr(hr.value); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } + worker.drawLine(im.xorigin, y, im.xorigin + im.xsize, y, hr.color, hr.stroke); + } + } else if (pe instanceof VRule) { + VRule vr = (VRule) pe; + if (vr.timestamp >= im.start && vr.timestamp <= im.end) { + int x = mapper.xtr(vr.timestamp); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } + worker.drawLine(x, im.yorigin, x, im.yorigin - im.ysize, vr.color, vr.stroke); + } + } + else if (pe instanceof HSpan) { + HSpan hr = (HSpan) pe; + int ys = mapper.ytr(hr.start); + int ye = mapper.ytr(hr.end); + int height = ys - ye; + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } + worker.fillRect(im.xorigin, ys - height, im.xsize, height, hr.color); + } else if (pe instanceof VSpan) { + VSpan vr = (VSpan) pe; + int xs = mapper.xtr(vr.start); + int xe = mapper.xtr(vr.end); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } + worker.fillRect(xs, im.yorigin - im.ysize, xe - xs, im.ysize, vr.color); + } + } + if (found) + worker.reset(); + } + + private void drawText() { + if (!gdef.onlyGraph) { + worker.setTextAntiAliasing(gdef.textAntiAliasing); + if (gdef.title != null) { + // I2P truncate on the right only + //int x = im.xgif / 2 - (int) (worker.getStringWidth(gdef.title, gdef.getFont(RrdGraphConstants.FONTTAG_TITLE)) / 2); + int x = Math.max(2, im.xgif / 2 - (int) (worker.getStringWidth(gdef.title, gdef.getFont(RrdGraphConstants.FONTTAG_TITLE)) / 2)); + // I2P a little less padding on top and more on the bottom + //int y = PADDING_TOP + (int) worker.getFontAscent(gdef.getFont(FONTTAG_TITLE)); + int y = RrdGraphConstants.PADDING_TOP * 2 / 3 + (int) worker.getFontAscent(gdef.getFont(RrdGraphConstants.FONTTAG_TITLE)); + worker.drawString(gdef.title, x, y, gdef.getFont(RrdGraphConstants.FONTTAG_TITLE), gdef.getColor(ElementsNames.font)); + } + if (gdef.verticalLabel != null) { + int y = im.yorigin - im.ysize / 2 + (int) worker.getStringWidth(gdef.verticalLabel, gdef.getFont(RrdGraphConstants.FONTTAG_UNIT)) / 2; + int ascent = (int) worker.getFontAscent(gdef.getFont(RrdGraphConstants.FONTTAG_UNIT)); + worker.transform(RrdGraphConstants.PADDING_LEFT, y, -Math.PI / 2); + worker.drawString(gdef.verticalLabel, 0, ascent, + gdef.getFont(RrdGraphConstants.FONTTAG_UNIT), + gdef.getColor(ElementsNames.font)); + worker.reset(); + } + worker.setTextAntiAliasing(false); + } + } + + private void drawGrid() { + if (!gdef.onlyGraph) { + worker.setTextAntiAliasing(gdef.textAntiAliasing); + Paint shade1 = gdef.getColor(ElementsNames.shadea); + Paint shade2 = gdef.getColor(ElementsNames.shadeb); + Stroke borderStroke = new BasicStroke(1); + worker.drawLine(0, 0, im.xgif - 1, 0, shade1, borderStroke); + worker.drawLine(1, 1, im.xgif - 2, 1, shade1, borderStroke); + worker.drawLine(0, 0, 0, im.ygif - 1, shade1, borderStroke); + worker.drawLine(1, 1, 1, im.ygif - 2, shade1, borderStroke); + worker.drawLine(im.xgif - 1, 0, im.xgif - 1, im.ygif - 1, shade2, borderStroke); + worker.drawLine(0, im.ygif - 1, im.xgif - 1, im.ygif - 1, shade2, borderStroke); + worker.drawLine(im.xgif - 2, 1, im.xgif - 2, im.ygif - 2, shade2, borderStroke); + worker.drawLine(1, im.ygif - 2, im.xgif - 2, im.ygif - 2, shade2, borderStroke); + if (gdef.drawXGrid) { + new TimeAxis(this).draw(); + } + if (gdef.drawYGrid) { + boolean ok; + if (gdef.altYMrtg) { + ok = new ValueAxisMrtg(this).draw(); + } else if (gdef.logarithmic) { + ok = new ValueAxisLogarithmic(this, worker, gdef.locale).draw(); + } else { + ok = new ValueAxis(this).draw(); + } + if (!ok) { + String msg = "No Data Found"; + worker.drawString(msg, im.xgif / 2 - (int) worker.getStringWidth(msg, + gdef.getFont(RrdGraphConstants.FONTTAG_TITLE)) / 2, + (2 * im.yorigin - im.ysize) / 2, + gdef.getFont(RrdGraphConstants.FONTTAG_TITLE), gdef.getColor(ElementsNames.font)); + } + } + worker.setTextAntiAliasing(false); + } + } + + private void drawData() { + worker.setAntiAliasing(gdef.antiAliasing); + worker.clip(im.xorigin, im.yorigin - gdef.height - 1, gdef.width, + gdef.height + 2); + double areaZero = mapper.ytr((im.minval > 0.0) ? im.minval : Math.min(im.maxval, 0.0)); + double[] x = gdef.downsampler == null ? xtr(dproc.getTimestamps()) : null; + double[] lastY = null; + // draw line, area and stack + for (PlotElement plotElement : gdef.plotElements) { + if (plotElement instanceof SourcedPlotElement) { + SourcedPlotElement source = (SourcedPlotElement) plotElement; + double[] y; + if (gdef.downsampler != null) { + DownSampler.DataSet set = gdef.downsampler.downsize(dproc.getTimestamps(), + source.getValues()); + x = xtr(set.timestamps); + y = ytr(set.values); + } else { + y = ytr(source.getValues()); + } + if (Line.class.isAssignableFrom(source.getClass())) { + worker.drawPolyline(x, y, source.color, ((Line) source).stroke); + } else if (Area.class.isAssignableFrom(source.getClass())) { + if (source.parent == null) { + worker.fillPolygon(x, areaZero, y, source.color); + } else { + worker.fillPolygon(x, lastY, y, source.color); + worker.drawPolyline(x, lastY, source.getParentColor(), new BasicStroke(0)); + } + } else if (source instanceof Stack) { + Stack stack = (Stack) source; + float width = stack.getParentLineWidth(); + if (width >= 0F) { + // line + worker.drawPolyline(x, y, stack.color, new BasicStroke(width)); + } else { + // area + worker.fillPolygon(x, lastY, y, stack.color); + worker.drawPolyline(x, lastY, stack.getParentColor(), new BasicStroke(0)); + } + } else { + // should not be here + throw new IllegalStateException("Unknown plot source: " + source.getClass().getName()); + } + lastY = y; + } + } + worker.reset(); + worker.setAntiAliasing(false); + } + + private void drawAxis() { + if (!gdef.onlyGraph) { + Paint gridColor = gdef.getColor(ElementsNames.grid); + Paint xaxisColor = gdef.getColor(ElementsNames.xaxis); + Paint yaxisColor = gdef.getColor(ElementsNames.yaxis); + Paint arrowColor = gdef.getColor(ElementsNames.arrow); + Stroke stroke = new BasicStroke(1); + worker.drawLine(im.xorigin + im.xsize, im.yorigin, + im.xorigin + im.xsize, im.yorigin - im.ysize, gridColor, + stroke); + worker.drawLine(im.xorigin, im.yorigin - im.ysize, + im.xorigin + im.xsize, im.yorigin - im.ysize, gridColor, + stroke); + worker.drawLine(im.xorigin - 4, im.yorigin, im.xorigin + im.xsize + 4, + im.yorigin, xaxisColor, stroke); + worker.drawLine(im.xorigin, im.yorigin + 4, im.xorigin, + im.yorigin - im.ysize - 4, yaxisColor, stroke); + + // I2P skip arrowheads if transparent + if (((Color)arrowColor).getAlpha() == 0) + return; + + //Do X axis arrow + double[] xArrowX = { im.xorigin + im.xsize + 4, + im.xorigin + im.xsize + 9, im.xorigin + im.xsize + 4, }; + double[] xArrowY = { im.yorigin - 3, im.yorigin, im.yorigin + 3, }; + worker.fillPolygon(xArrowX, im.yorigin + 3.0, xArrowY, arrowColor); + + //Do y axis arrow + double[] yArrowX = { im.xorigin - 3, im.xorigin, im.xorigin + 3, }; + double[] yArrowY = { im.yorigin - im.ysize - 4, + im.yorigin - im.ysize - 9, im.yorigin - im.ysize - 4, }; + worker.fillPolygon(yArrowX, im.yorigin - im.ysize - 4.0, yArrowY, arrowColor); + } + } + + private void drawBackground() throws IOException { + worker.fillRect(0, 0, im.xgif, im.ygif, gdef.getColor(ElementsNames.back)); + if (gdef.backgroundImage != null) { + worker.loadImage(gdef.backgroundImage, 0, 0, im.xgif, im.ygif); + } + if (gdef.canvasImage != null) { + worker.loadImage(gdef.canvasImage, im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize); + } + worker.fillRect(im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize, gdef.getColor(ElementsNames.canvas)); + } + + private void createImageWorker() { + worker.resize(im.xgif, im.ygif); + } + + private void placeLegends() { + if (!gdef.noLegend && !gdef.onlyGraph) { + int border = (int) (getFontCharWidth(RrdGraphConstants.FontTag.LEGEND) * RrdGraphConstants.PADDING_LEGEND); + LegendComposer lc = new LegendComposer(this, border, im.ygif, im.xgif - 2 * border); + im.ygif = lc.placeComments() + RrdGraphConstants.PADDING_BOTTOM; + } + } + + private void initializeLimits() { + im.xsize = gdef.width; + im.ysize = gdef.height; + im.unitslength = gdef.unitsLength; + + if (gdef.onlyGraph) { + im.xorigin = 0; + } else { + im.xorigin = (int) (RrdGraphConstants.PADDING_LEFT + im.unitslength * getFontCharWidth( + RrdGraphConstants.FONTTAG_AXIS)); + } + + if (!gdef.onlyGraph && gdef.verticalLabel != null) { + im.xorigin += (int) getFontHeight(RrdGraphConstants.FONTTAG_UNIT); + } + + if (gdef.onlyGraph) { + im.yorigin = im.ysize; + } else { + im.yorigin = RrdGraphConstants.PADDING_TOP + im.ysize; + } + + if (!gdef.onlyGraph && gdef.title != null) { + im.yorigin += (int) (getFontHeight(RrdGraphConstants.FONTTAG_TITLE) + RrdGraphConstants.PADDING_TITLE); + } + + if (gdef.onlyGraph) { + im.xgif = im.xsize; + im.ygif = im.yorigin; + } else { + im.xgif = RrdGraphConstants.PADDING_RIGHT + im.xsize + im.xorigin; + im.ygif = im.yorigin + (int) (RrdGraphConstants.PADDING_PLOT * getFontHeight( + RrdGraphConstants.FONTTAG_AXIS)); + } + } + + private void removeOutOfRangeRules() { + for (PlotElement plotElement : gdef.plotElements) { + if (plotElement instanceof HRule) { + ((HRule) plotElement).setLegendVisibility(im.minval, im.maxval, gdef.forceRulesLegend); + } else if (plotElement instanceof VRule) { + ((VRule) plotElement).setLegendVisibility(im.start, im.end, gdef.forceRulesLegend); + } + } + } + + private void removeOutOfRangeSpans() { + for (PlotElement plotElement : gdef.plotElements) { + if (plotElement instanceof HSpan) { + ((HSpan) plotElement).setLegendVisibility(im.minval, im.maxval, gdef.forceRulesLegend); + } else if (plotElement instanceof VSpan) { + ((VSpan) plotElement).setLegendVisibility(im.start, im.end, gdef.forceRulesLegend); + } + } + } + + private void expandValueRange() { + im.ygridstep = (gdef.valueAxisSetting != null) ? + gdef.valueAxisSetting.gridStep : + Double.NaN; + im.ylabfact = (gdef.valueAxisSetting != null) ? + gdef.valueAxisSetting.labelFactor : + 0; + if (!gdef.rigid && !gdef.logarithmic) { + double scaledMin; + double scaledMax; + double adj; + if (Double.isNaN(im.ygridstep)) { + if (gdef.altYMrtg) { /* mrtg */ + im.decimals = Math.ceil( + Math.log10(Math.max(Math.abs(im.maxval), Math.abs(im.minval)))); + im.quadrant = 0; + if (im.minval < 0) { + im.quadrant = 2; + if (im.maxval <= 0) { + im.quadrant = 4; + } + } + switch (im.quadrant) { + case 2: + im.scaledstep = Math.ceil( + 50 * Math.pow(10, -(im.decimals)) * Math.max(Math.abs(im.maxval), + Math.abs(im.minval))) * Math.pow(10, im.decimals - 2); + scaledMin = -2 * im.scaledstep; + scaledMax = 2 * im.scaledstep; + break; + case 4: + im.scaledstep = Math.ceil( + 25 * Math.pow(10, -(im.decimals)) * Math.abs(im.minval)) * Math.pow( + 10, im.decimals - 2); + scaledMin = -4 * im.scaledstep; + scaledMax = 0; + break; + default: /* quadrant 0 */ + im.scaledstep = Math.ceil( + 25 * Math.pow(10, -(im.decimals)) * im.maxval) * Math.pow(10, + im.decimals - 2); + scaledMin = 0; + scaledMax = 4 * im.scaledstep; + break; + } + im.minval = scaledMin; + im.maxval = scaledMax; + } else if (gdef.altAutoscale || (gdef.altAutoscaleMin && gdef.altAutoscaleMax)) { + /* measure the amplitude of the function. Make sure that + graph boundaries are slightly higher than max/min vals, + so we can see amplitude on the graph */ + double delt; + double fact; + + delt = im.maxval - im.minval; + adj = delt * 0.1; + fact = 2.0 * Math.pow(10.0, Math.floor( + Math.log10(Math.max(Math.abs(im.minval), Math.abs(im.maxval)))) - 2); + if (delt < fact) { + adj = (fact - delt) * 0.55; + } + im.minval -= adj; + im.maxval += adj; + } else if (gdef.altAutoscaleMin) { + /* measure the amplitude of the function. Make sure that + graph boundaries are slightly lower than min vals, + so we can see amplitude on the graph */ + adj = (im.maxval - im.minval) * 0.1; + im.minval -= adj; + } else if (gdef.altAutoscaleMax) { + /* measure the amplitude of the function. Make sure that + graph boundaries are slightly higher than max vals, + so we can see amplitude on the graph */ + adj = (im.maxval - im.minval) * 0.1; + im.maxval += adj; + } else { + scaledMin = im.minval / im.magfact; + scaledMax = im.maxval / im.magfact; + for (int i = 1; SENSIBLE_VALUES[i] > 0; i++) { + if (SENSIBLE_VALUES[i - 1] >= scaledMin && SENSIBLE_VALUES[i] <= scaledMin) { + im.minval = SENSIBLE_VALUES[i] * im.magfact; + } + if (-SENSIBLE_VALUES[i - 1] <= scaledMin && -SENSIBLE_VALUES[i] >= scaledMin) { + im.minval = -SENSIBLE_VALUES[i - 1] * im.magfact; + } + if (SENSIBLE_VALUES[i - 1] >= scaledMax && SENSIBLE_VALUES[i] <= scaledMax) { + im.maxval = SENSIBLE_VALUES[i - 1] * im.magfact; + } + if (-SENSIBLE_VALUES[i - 1] <= scaledMax && -SENSIBLE_VALUES[i] >= scaledMax) { + im.maxval = -SENSIBLE_VALUES[i] * im.magfact; + } + } + } + } else { + im.minval = im.ylabfact * im.ygridstep * Math.floor( + im.minval / (im.ylabfact * im.ygridstep)); + im.maxval = im.ylabfact * im.ygridstep * Math.ceil( + im.maxval / (im.ylabfact * im.ygridstep)); + } + + } + } + + private void identifySiUnit() { + im.unitsexponent = gdef.unitsExponent; + im.base = gdef.base; + if (!gdef.logarithmic) { + double digits; + if (im.unitsexponent != Integer.MAX_VALUE) { + digits = Math.floor(im.unitsexponent / 3.0); + } else { + digits = Math.floor( + Math.log(Math.max(Math.abs(im.minval), Math.abs(im.maxval))) / Math.log( + im.base)); + } + im.magfact = Math.pow(im.base, digits); + if (((digits + SYMBOLS_CENTER) < SYMBOLS.length) && ((digits + SYMBOLS_CENTER) >= 0)) { + im.symbol = SYMBOLS[(int) digits + SYMBOLS_CENTER]; + } else { + im.symbol = '?'; + } + } + } + + private void findMinMaxValues() { + double minval = Double.NaN; + double maxval = Double.NaN; + for (PlotElement pe : gdef.plotElements) { + if (pe instanceof SourcedPlotElement) { + minval = Util.min(((SourcedPlotElement) pe).getMinValue(), minval); + maxval = Util.max(((SourcedPlotElement) pe).getMaxValue(), maxval); + } + } + if (Double.isNaN(minval)) { + minval = 0D; + } + if (Double.isNaN(maxval)) { + maxval = 1D; + } + im.minval = gdef.minValue; + im.maxval = gdef.maxValue; + /* adjust min and max values */ + if (Double.isNaN( + im.minval) || ((!gdef.logarithmic && !gdef.rigid) && im.minval > minval)) { + im.minval = minval; + } + if (Double.isNaN(im.maxval) || (!gdef.rigid && im.maxval < maxval)) { + if (gdef.logarithmic) { + im.maxval = maxval * 1.1; + } else { + im.maxval = maxval; + } + } + /* make sure min is smaller than max */ + if (im.minval > im.maxval) { + im.minval = 0.99 * im.maxval; + } + /* make sure min and max are not equal */ + if (Math.abs(im.minval - im.maxval) < .0000001) { + im.maxval *= 1.01; + if (!gdef.logarithmic) { + im.minval *= 0.99; + } + /* make sure min and max are not both zero */ + if (im.maxval == 0.0) { + im.maxval = 1.0; + } + } + if (gdef.logarithmic) { + im.log = LogService.resolve(im); + } + } + + private void calculatePlotValues() { + for (PlotElement pe : gdef.plotElements) { + if (pe instanceof SourcedPlotElement) { + ((SourcedPlotElement) pe).assignValues(dproc); + } + } + } + + private void resolveTextElements() { + ValueScaler valueScaler = new ValueScaler(gdef.base); + for (CommentText comment : gdef.comments) { + comment.resolveText(gdef.locale, dproc, valueScaler); + } + } + + private void fetchData() throws IOException { + dproc.setPixelCount(gdef.width); + if (gdef.poolUsed) { + dproc.setPoolUsed(true); + dproc.setPool(gdef.getPool()); + } + dproc.setTimeZone(gdef.tz); + if (gdef.step > 0) { + dproc.setStep(gdef.step); + dproc.setFetchRequestResolution(gdef.step); + } + for (Source src : gdef.sources) { + src.requestData(dproc); + } + dproc.processData(); + im.start = gdef.startTime; + im.end = gdef.endTime; + } + + private boolean lazyCheck() throws IOException { + // redraw if lazy option is not set or file does not exist + if (!gdef.lazy || !Util.fileExists(gdef.filename)) { + return false; // 'false' means 'redraw' + } + // redraw if not enough time has passed + long secPerPixel = (gdef.endTime - gdef.startTime) / gdef.width; + long elapsed = Util.getTimestamp() - Util.getLastModifiedTime(gdef.filename); + return elapsed <= secPerPixel; + } + + private void drawLegend() { + if (!gdef.onlyGraph && !gdef.noLegend) { + worker.setTextAntiAliasing(gdef.textAntiAliasing); + int ascent = (int) worker.getFontAscent(gdef.getFont(RrdGraphConstants.FONTTAG_LEGEND)); + int box = (int) getBox(); + int boxSpace = (int) (getBoxSpace()); + for (CommentText c : gdef.comments) { + if (c.isValidGraphElement()) { + int x = c.x; + int y = c.y + ascent; + if (c instanceof LegendText) { + // draw with BOX + worker.fillRect(x, y - box, box, box, gdef.getColor(ElementsNames.frame)); + Paint bc = gdef.getColor(ElementsNames.back); + Paint lc = ((LegendText) c).legendColor; + boolean bt = bc.getTransparency() != Transparency.OPAQUE; + boolean lt = lc.getTransparency() != Transparency.OPAQUE; + // no use drawing unless both the two on top have some transparency + if (bt && lt) + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, gdef.getColor(ElementsNames.canvas)); + // no use drawing unless the one on top has some transparency + if (lt) + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, bc); + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, lc); + worker.drawString(c.resolvedText, x + boxSpace, y, + gdef.getFont(RrdGraphConstants.FONTTAG_LEGEND), + gdef.getColor(ElementsNames.font)); + } else { + worker.drawString(c.resolvedText, x, y, gdef.getFont(RrdGraphConstants.FONTTAG_LEGEND), + gdef.getColor(ElementsNames.font)); + } + } + } + worker.setTextAntiAliasing(false); + } + } + + // helper methods + + double getFontHeight(RrdGraphConstants.FontTag fonttag) { + return worker.getFontHeight(gdef.getFont(fonttag)); + } + + double getFontCharWidth(RrdGraphConstants.FontTag fonttag) { + return worker.getStringWidth("a", gdef.getFont(fonttag)); + } + + double getInterlegendSpace() { + return getFontCharWidth(RrdGraphConstants.FONTTAG_LEGEND) * RrdGraphConstants.LEGEND_INTERSPACING; + } + + double getLeading() { + return getFontHeight(RrdGraphConstants.FONTTAG_LEGEND) * RrdGraphConstants.LEGEND_LEADING; + } + + double getSmallLeading() { + return getFontHeight(RrdGraphConstants.FONTTAG_LEGEND) * RrdGraphConstants.LEGEND_LEADING_SMALL; + } + + double getBoxSpace() { + return Math.ceil(getFontHeight(RrdGraphConstants.FONTTAG_LEGEND) * RrdGraphConstants.LEGEND_BOX_SPACE); + } + + private double getBox() { + return getFontHeight(RrdGraphConstants.FONTTAG_LEGEND) * RrdGraphConstants.LEGEND_BOX; + } + + private double[] xtr(long[] timestamps) { + double[] timestampsDev = new double[2 * timestamps.length - 1]; + for (int i = 0, j = 0; i < timestamps.length; i += 1, j += 2) { + timestampsDev[j] = mapper.xtr(timestamps[i]); + if (i < timestamps.length - 1) { + timestampsDev[j + 1] = timestampsDev[j]; + } + } + return timestampsDev; + } + + private double[] ytr(double[] values) { + double[] valuesDev = new double[2 * values.length - 1]; + for (int i = 0, j = 0; i < values.length; i += 1, j += 2) { + if (Double.isNaN(values[i])) { + valuesDev[j] = Double.NaN; + } else { + valuesDev[j] = mapper.ytr(values[i]); + } + if (j > 0) { + valuesDev[j - 1] = valuesDev[j]; + } + } + return valuesDev; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/graph/TimeAxis.java b/apps/jrobin/java/src/org/rrd4j/graph/TimeAxis.java index 17aa5ab04..89635519b 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/TimeAxis.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/TimeAxis.java @@ -8,21 +8,20 @@ import java.util.Date; class TimeAxis extends Axis { private static final TimeAxisSetting[] tickSettings = { - new TimeAxisSetting(0, SECOND, 30, MINUTE, 5, MINUTE, 5, 0, HH_MM), - new TimeAxisSetting(2, MINUTE, 1, MINUTE, 5, MINUTE, 5, 0, HH_MM), - new TimeAxisSetting(5, MINUTE, 2, MINUTE, 10, MINUTE, 10, 0, HH_MM), - new TimeAxisSetting(10, MINUTE, 5, MINUTE, 20, MINUTE, 20, 0, HH_MM), - new TimeAxisSetting(30, MINUTE, 10, HOUR, 1, HOUR, 1, 0, HH_MM), - new TimeAxisSetting(60, MINUTE, 30, HOUR, 2, HOUR, 2, 0, HH_MM), - new TimeAxisSetting(180, HOUR, 1, HOUR, 6, HOUR, 6, 0, HH_MM), - new TimeAxisSetting(600, HOUR, 6, DAY, 1, DAY, 1, 24 * 3600, "EEE dd"), - new TimeAxisSetting(1800, HOUR, 12, DAY, 1, DAY, 2, 24 * 3600, "EEE dd"), - new TimeAxisSetting(3600, DAY, 1, WEEK, 1, WEEK, 1, 7 * 24 * 3600, "'Week 'w"), - new TimeAxisSetting(3 * 3600L, WEEK, 1, MONTH, 1, WEEK, 2, 7 * 24 * 3600, "'Week 'w"), - new TimeAxisSetting(6 * 3600L, MONTH, 1, MONTH, 1, MONTH, 1, 30 * 24 * 3600, "MMM"), - new TimeAxisSetting(48 * 3600L, MONTH, 1, MONTH, 3, MONTH, 3, 30 * 24 * 3600, "MMM"), - new TimeAxisSetting(10 * 24 * 3600L, YEAR, 1, YEAR, 1, YEAR, 1, 365 * 24 * 3600, "yy"), - new TimeAxisSetting(-1, MONTH, 0, MONTH, 0, MONTH, 0, 0, "") + new TimeAxisSetting(0, TimeUnit.SECOND, 30, TimeUnit.MINUTE, 5, TimeUnit.MINUTE, 5, 0), + new TimeAxisSetting(2, TimeUnit.MINUTE, 1, TimeUnit.MINUTE, 5, TimeUnit.MINUTE, 5, 0), + new TimeAxisSetting(5, TimeUnit.MINUTE, 2, TimeUnit.MINUTE, 10, TimeUnit.MINUTE, 10, 0), + new TimeAxisSetting(10, TimeUnit.MINUTE, 5, TimeUnit.MINUTE, 20, TimeUnit.MINUTE, 20, 0), + new TimeAxisSetting(30, TimeUnit.MINUTE, 10, TimeUnit.HOUR, 1, TimeUnit.HOUR, 1, 0), + new TimeAxisSetting(60, TimeUnit.MINUTE, 30, TimeUnit.HOUR, 2, TimeUnit.HOUR, 2, 0), + new TimeAxisSetting(180, TimeUnit.HOUR, 1, TimeUnit.HOUR, 6, TimeUnit.HOUR, 6, 0), + new TimeAxisSetting(600, TimeUnit.HOUR, 6, TimeUnit.DAY, 1, TimeUnit.DAY, 1, 24 * 3600), + new TimeAxisSetting(1800, TimeUnit.HOUR, 12, TimeUnit.DAY, 1, TimeUnit.DAY, 2, 24 * 3600), + new TimeAxisSetting(3600, TimeUnit.DAY, 1, TimeUnit.WEEK, 1, TimeUnit.WEEK, 1, 7 * 24 * 3600), + new TimeAxisSetting(3 * 3600L, TimeUnit.WEEK, 1, TimeUnit.MONTH, 1, TimeUnit.WEEK, 2, 7 * 24 * 3600), + new TimeAxisSetting(6 * 3600L, TimeUnit.MONTH, 1, TimeUnit.MONTH, 1, TimeUnit.MONTH, 1, 30 * 24 * 3600), + new TimeAxisSetting(48 * 3600L, TimeUnit.MONTH, 1, TimeUnit.MONTH, 3, TimeUnit.MONTH, 3, 30 * 24 * 3600), + new TimeAxisSetting(10 * 24 * 3600L, TimeUnit.YEAR, 1, TimeUnit.YEAR, 1, TimeUnit.YEAR, 1, 365 * 24 * 3600), }; private final ImageParameters im; @@ -34,18 +33,27 @@ class TimeAxis extends Axis { private final double secPerPix; private final Calendar calendar; - TimeAxis(RrdGraph rrdGraph) { - this(rrdGraph, rrdGraph.worker); - } - /** - * Used for test + * Used for tests + * + * @param rrdGraph + * @param worker */ TimeAxis(RrdGraph rrdGraph, ImageWorker worker) { this.im = rrdGraph.im; this.worker = worker; this.gdef = rrdGraph.gdef; - this.mapper = rrdGraph.mapper; + this.mapper = new Mapper(this.gdef, this.im); + this.secPerPix = (im.end - im.start) / (double) im.xsize; + this.calendar = Calendar.getInstance(gdef.tz, gdef.locale); + this.calendar.setFirstDayOfWeek(gdef.firstDayOfWeek); + } + + TimeAxis(RrdGraphGenerator generator) { + this.im = generator.im; + this.worker = generator.worker; + this.gdef = generator.gdef; + this.mapper = generator.mapper; this.secPerPix = (im.end - im.start) / (double) im.xsize; this.calendar = Calendar.getInstance(gdef.tz, gdef.locale); this.calendar.setFirstDayOfWeek(gdef.firstDayOfWeek); @@ -67,8 +75,6 @@ class TimeAxis extends Axis { private void drawMinor() { if (!gdef.noMinorGrid) { - // I2P skip ticks if zero width - boolean ticks = ((BasicStroke)gdef.tickStroke).getLineWidth() > 0; adjustStartingTime(tickSetting.minorUnit, tickSetting.minorUnitCount); Paint color = gdef.getColor(ElementsNames.grid); int y0 = im.yorigin, y1 = y0 - im.ysize; @@ -76,8 +82,10 @@ class TimeAxis extends Axis { if (status == 0) { long time = calendar.getTime().getTime() / 1000L; int x = mapper.xtr(time); - if (ticks) + // skip ticks if zero width + if (gdef.drawTicks()) { worker.drawLine(x, y0 - 1, x, y0 + 1, color, gdef.tickStroke); + } worker.drawLine(x, y0, x, y1, color, gdef.gridStroke); } findNextTime(tickSetting.minorUnit, tickSetting.minorUnitCount); @@ -86,8 +94,6 @@ class TimeAxis extends Axis { } private void drawMajor() { - // I2P skip ticks if zero width - boolean ticks = ((BasicStroke)gdef.tickStroke).getLineWidth() > 0; adjustStartingTime(tickSetting.majorUnit, tickSetting.majorUnitCount); Paint color = gdef.getColor(ElementsNames.mgrid); int y0 = im.yorigin, y1 = y0 - im.ysize; @@ -95,8 +101,10 @@ class TimeAxis extends Axis { if (status == 0) { long time = calendar.getTime().getTime() / 1000L; int x = mapper.xtr(time); - if (ticks) + // skip ticks if zero width + if (gdef.drawTicks()) { worker.drawLine(x, y0 - 2, x, y0 + 2, color, gdef.tickStroke); + } worker.drawLine(x, y0, x, y1, color, gdef.gridStroke); } findNextTime(tickSetting.majorUnit, tickSetting.majorUnitCount); @@ -122,7 +130,7 @@ class TimeAxis extends Axis { } } - private void findNextTime(int timeUnit, int timeUnitCount) { + private void findNextTime(TimeUnit timeUnit, int timeUnitCount) { switch (timeUnit) { case SECOND: calendar.add(Calendar.SECOND, timeUnitCount); @@ -153,7 +161,7 @@ class TimeAxis extends Axis { return (time < im.start) ? -1 : (time > im.end) ? +1 : 0; } - private void adjustStartingTime(int timeUnit, int timeUnitCount) { + private void adjustStartingTime(TimeUnit timeUnit, int timeUnitCount) { calendar.setTime(new Date(im.start * 1000L)); switch (timeUnit) { case SECOND: @@ -204,14 +212,19 @@ class TimeAxis extends Axis { private void chooseTickSettings() { if (gdef.timeAxisSetting != null) { tickSetting = new TimeAxisSetting(gdef.timeAxisSetting); - } - else { - for (int i = 0; tickSettings[i].secPerPix >= 0 && secPerPix > tickSettings[i].secPerPix; i++) { - tickSetting = tickSettings[i]; + } else { + for (TimeAxisSetting i: tickSettings) { + if (secPerPix < i.secPerPix) { + break; + } else { + tickSetting = i; + } } } if (gdef.timeLabelFormat != null) { tickSetting = tickSetting.withLabelFormat(gdef.timeLabelFormat); + } else { + gdef.formatProvider.apply(tickSetting.labelUnit).ifPresent(f -> tickSetting = tickSetting.withLabelFormat(f)); } } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/TimeAxisSetting.java b/apps/jrobin/java/src/org/rrd4j/graph/TimeAxisSetting.java index 0fb1cc3f5..17132cf5b 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/TimeAxisSetting.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/TimeAxisSetting.java @@ -2,12 +2,17 @@ package org.rrd4j.graph; class TimeAxisSetting { final long secPerPix; - final int minorUnit, minorUnitCount, majorUnit, majorUnitCount; - final int labelUnit, labelUnitCount, labelSpan; + final TimeUnit majorUnit; + final int majorUnitCount; + final TimeUnit minorUnit; + final int minorUnitCount; + final TimeUnit labelUnit; + final int labelUnitCount; + final int labelSpan; final TimeLabelFormat format; - TimeAxisSetting(long secPerPix, int minorUnit, int minorUnitCount, int majorUnit, int majorUnitCount, - int labelUnit, int labelUnitCount, int labelSpan, TimeLabelFormat format) { + TimeAxisSetting(long secPerPix, TimeUnit minorUnit, int minorUnitCount, TimeUnit majorUnit, int majorUnitCount, + TimeUnit labelUnit, int labelUnitCount, int labelSpan, TimeLabelFormat format) { this.secPerPix = secPerPix; this.minorUnit = minorUnit; this.minorUnitCount = minorUnitCount; @@ -19,6 +24,19 @@ class TimeAxisSetting { this.format = format; } + TimeAxisSetting(long secPerPix, TimeUnit minorUnit, int minorUnitCount, TimeUnit majorUnit, int majorUnitCount, + TimeUnit labelUnit, int labelUnitCount, int labelSpan) { + this.secPerPix = secPerPix; + this.minorUnit = minorUnit; + this.minorUnitCount = minorUnitCount; + this.majorUnit = majorUnit; + this.majorUnitCount = majorUnitCount; + this.labelUnit = labelUnit; + this.labelUnitCount = labelUnitCount; + this.labelSpan = labelSpan; + this.format = new SimpleTimeLabelFormat(labelUnit.getLabel()); + } + TimeAxisSetting(TimeAxisSetting s) { this.secPerPix = s.secPerPix; this.minorUnit = s.minorUnit; @@ -33,20 +51,8 @@ class TimeAxisSetting { TimeAxisSetting(int minorUnit, int minorUnitCount, int majorUnit, int majorUnitCount, int labelUnit, int labelUnitCount, int labelSpan, TimeLabelFormat format) { - this(0, minorUnit, minorUnitCount, majorUnit, majorUnitCount, - labelUnit, labelUnitCount, labelSpan, format); - } - - TimeAxisSetting(long secPerPix, int minorUnit, int minorUnitCount, int majorUnit, int majorUnitCount, - int labelUnit, int labelUnitCount, int labelSpan, String format) { - this(secPerPix, minorUnit, minorUnitCount, majorUnit, majorUnitCount, - labelUnit, labelUnitCount, labelSpan, new SimpleTimeLabelFormat(format)); - } - - TimeAxisSetting(int minorUnit, int minorUnitCount, int majorUnit, int majorUnitCount, - int labelUnit, int labelUnitCount, int labelSpan, String format) { - this(0, minorUnit, minorUnitCount, majorUnit, majorUnitCount, - labelUnit, labelUnitCount, labelSpan, new SimpleTimeLabelFormat(format)); + this(0, TimeUnit.resolveUnit(minorUnit), minorUnitCount, TimeUnit.resolveUnit(majorUnit), majorUnitCount, + TimeUnit.resolveUnit(labelUnit), labelUnitCount, labelSpan, format); } TimeAxisSetting withLabelFormat(TimeLabelFormat f) { diff --git a/apps/jrobin/java/src/org/rrd4j/graph/TimeUnit.java b/apps/jrobin/java/src/org/rrd4j/graph/TimeUnit.java new file mode 100644 index 000000000..4bdefdc7b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/graph/TimeUnit.java @@ -0,0 +1,71 @@ +package org.rrd4j.graph; + +import java.util.Calendar; + +import static org.rrd4j.graph.RrdGraphConstants.HH_MM; + +public enum TimeUnit { + SECOND { + @Override + public String getLabel() { + return "s"; + } + }, + MINUTE { + @Override + public String getLabel() { + return HH_MM; + } + }, + HOUR { + @Override + public String getLabel() { + return HH_MM; + } + }, + DAY { + @Override + public String getLabel() { + return "EEE dd"; + } + }, + WEEK { + @Override + public String getLabel() { + return "'Week 'w"; + } + }, + MONTH { + @Override + public String getLabel() { + return "MMM"; + } + }, + YEAR { + @Override + public String getLabel() { + return "yy"; + } + }; + public abstract String getLabel(); + public static TimeUnit resolveUnit(int unitKey) { + switch (unitKey) { + case Calendar.SECOND: + return SECOND; + case Calendar.MINUTE: + return MINUTE; + case Calendar.HOUR_OF_DAY: + return HOUR; + case Calendar.DAY_OF_MONTH: + return DAY; + case Calendar.WEEK_OF_YEAR: + return WEEK; + case Calendar.MONTH: + return MONTH; + case Calendar.YEAR: + return YEAR; + default: + throw new IllegalArgumentException("Unidentified key " + unitKey); + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxis.java b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxis.java index 7ae9ce299..bb078a6a1 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxis.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxis.java @@ -37,15 +37,24 @@ class ValueAxis extends Axis { private final RrdGraphDef gdef; private final Mapper mapper; - ValueAxis(RrdGraph rrdGraph) { - this(rrdGraph, rrdGraph.worker); - } - + /** + * Used for tests + * + * @param rrdGraph + * @param worker + */ ValueAxis(RrdGraph rrdGraph, ImageWorker worker) { this.im = rrdGraph.im; this.gdef = rrdGraph.gdef; this.worker = worker; - this.mapper = rrdGraph.mapper; + this.mapper = new Mapper(this.gdef, this.im); + } + + ValueAxis(RrdGraphGenerator generator) { + this.im = generator.im; + this.gdef = generator.gdef; + this.worker = generator.worker; + this.mapper = generator.mapper; } boolean draw() { @@ -124,8 +133,6 @@ class ValueAxis extends Axis { int egrid = (int) (im.maxval / gridstep + 1); double scaledstep = gridstep / im.magfact; boolean fractional = isFractional(scaledstep, labfact); - // I2P skip ticks if zero width - boolean ticks = ((BasicStroke)gdef.tickStroke).getLineWidth() > 0; for (int i = sgrid; i <= egrid; i++) { int y = mapper.ytr(gridstep * i); if (y >= im.yorigin - im.ysize && y <= im.yorigin) { @@ -157,14 +164,16 @@ class ValueAxis extends Axis { } int length = (int) (worker.getStringWidth(graph_label, font)); worker.drawString(graph_label, x0 - length - PADDING_VLABEL, y + labelOffset, font, fontColor); - if (ticks) { + // skip ticks if zero width + if (gdef.drawTicks()) { worker.drawLine(x0 - 2, y, x0 + 2, y, mGridColor, gdef.tickStroke); worker.drawLine(x1 - 2, y, x1 + 2, y, mGridColor, gdef.tickStroke); } worker.drawLine(x0, y, x1, y, mGridColor, gdef.gridStroke); } else if (!(gdef.noMinorGrid)) { - if (ticks) { + // skip ticks if zero width + if (gdef.drawTicks()) { worker.drawLine(x0 - 1, y, x0 + 1, y, gridColor, gdef.tickStroke); worker.drawLine(x1 - 1, y, x1 + 1, y, gridColor, gdef.tickStroke); } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisLogarithmic.java b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisLogarithmic.java index 2d40f1134..720252c94 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisLogarithmic.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisLogarithmic.java @@ -1,8 +1,7 @@ package org.rrd4j.graph; -import org.rrd4j.core.Util; - import java.awt.*; +import java.util.Locale; class ValueAxisLogarithmic extends Axis { private static final double[][] yloglab = { @@ -15,22 +14,52 @@ class ValueAxisLogarithmic extends Axis { {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; + @FunctionalInterface + private interface IntDoubleLabelConsumer { + void accept(int a, double b, String formatPattern); + default void accept(int a, double b) { + accept(a, b, "%.0e"); + } + } + + @FunctionalInterface + private interface IntDoubleLineConsumer { + void accept(int a, double b, Paint color); + } + private final ImageParameters im; private final ImageWorker worker; private final RrdGraphDef gdef; private final int fontHeight; private final Mapper mapper; + private final Locale locale; - ValueAxisLogarithmic(RrdGraph rrdGraph) { - this(rrdGraph, rrdGraph.worker); + /** + * Used for tests + * + * @param rrdGraph + * @param worker + */ + ValueAxisLogarithmic(RrdGraph rrdGraph, ImageWorker worker, Locale locale) { + this.im = rrdGraph.im; + this.gdef = rrdGraph.gdef; + this.worker = worker; + this.fontHeight = (int) Math.ceil(worker.getFontHeight(gdef.getFont(FONTTAG_AXIS))); + this.mapper = new Mapper(this.gdef, this.im); + this.locale = locale; } - ValueAxisLogarithmic(RrdGraph rrdGraph, ImageWorker worker) { + ValueAxisLogarithmic(RrdGraphGenerator rrdGraph, ImageWorker worker, Locale locale) { this.im = rrdGraph.im; this.gdef = rrdGraph.gdef; this.worker = worker; this.fontHeight = (int) Math.ceil(worker.getFontHeight(gdef.getFont(FONTTAG_AXIS))); this.mapper = rrdGraph.mapper; + this.locale = locale; + } + + private double findStart(double positive, int idx) { + return Math.pow(10, im.log.applyAsDouble(positive) - im.log.applyAsDouble(positive) % im.log.applyAsDouble(yloglab[idx][0])); } boolean draw() { @@ -43,21 +72,23 @@ class ValueAxisLogarithmic extends Axis { if (im.maxval == im.minval) { return false; } - double pixpex = im.ysize / (log10(im.maxval) - log10(im.minval)); + double pixpex = im.ysize / (im.log.applyAsDouble(im.maxval) - im.log.applyAsDouble(im.minval)); if (Double.isNaN(pixpex)) { return false; } - double minstep, pixperstep; - int minoridx = 0, majoridx = 0; + int minoridx = 0; + int majoridx = 0; + + // Find the index in yloglab for major and minor grid for (int i = 0; yloglab[i][0] > 0; i++) { - minstep = log10(yloglab[i][0]); + double minstep = Math.log10(yloglab[i][0]); for (int ii = 1; yloglab[i][ii + 1] > 0; ii++) { if (yloglab[i][ii + 2] == 0) { - minstep = log10(yloglab[i][ii + 1]) - log10(yloglab[i][ii]); + minstep = Math.log10(yloglab[i][ii + 1]) - Math.log10(yloglab[i][ii]); break; } } - pixperstep = pixpex * minstep; + double pixperstep = pixpex * minstep; if (pixperstep > 5) { minoridx = i; } @@ -66,14 +97,32 @@ class ValueAxisLogarithmic extends Axis { } } - // Draw minor grid for positive values - double positiveMin = (im.minval > 0.0) ? im.minval : 0.0; - int x0 = im.xorigin, x1 = x0 + im.xsize; + double positiveMin = Math.max(im.minval, 0.0); + int x0 = im.xorigin; + int x1 = x0 + im.xsize; if (yloglab[minoridx][0] == 0 || yloglab[majoridx][0] == 0) { return false; } - for (double value = Math.pow(10, log10(positiveMin) - - log10(positiveMin) % log10(yloglab[minoridx][0])); + String zeroFormatted = String.format(locale, "%.0e", 0.0); + IntDoubleLabelConsumer drawAxisLabel = (y, v, f) -> { + String graphLabel = String.format(locale, f, v); + if (zeroFormatted.equals(graphLabel)) { + graphLabel = String.format(locale, "%.0f", v); + } + int length = (int) (worker.getStringWidth(graphLabel, font)); + worker.drawString(graphLabel, x0 - length - PADDING_VLABEL, y + labelOffset, font, fontColor); + }; + IntDoubleLineConsumer drawAxisLines = (y, v, p) -> { + if (gdef.drawTicks()) { + worker.drawLine(x0 - 1, y, x0 + 1, y, p, gdef.tickStroke); + worker.drawLine(x1 - 1, y, x1 + 1, y, p, gdef.tickStroke); + } + worker.drawLine(x0, y, x1, y, p, gdef.gridStroke); + }; + + + // Draw minor grid for positive values + for (double value = findStart(positiveMin, minoridx); value <= im.maxval; value *= yloglab[minoridx][0]) { if (value < positiveMin) continue; @@ -83,16 +132,13 @@ class ValueAxisLogarithmic extends Axis { if (y <= im.yorigin - im.ysize) { break; } - worker.drawLine(x0 - 1, y, x0 + 1, y, gridColor, gdef.tickStroke); - worker.drawLine(x1 - 1, y, x1 + 1, y, gridColor, gdef.tickStroke); - worker.drawLine(x0, y, x1, y, gridColor, gdef.gridStroke); + drawAxisLines.accept(y, value, gridColor); } } // Draw minor grid for negative values - double negativeMin = -1.0 * ((im.maxval < 0.0) ? im.maxval : 0.0); - for (double value = Math.pow(10, log10(negativeMin) - - log10(negativeMin) % log10(yloglab[minoridx][0])); + double negativeMin = -1.0 * (Math.min(im.maxval, 0.0)); + for (double value = findStart(negativeMin, minoridx); value <= -1.0 * im.minval; value *= yloglab[minoridx][0]) { if (value < negativeMin) continue; @@ -102,9 +148,7 @@ class ValueAxisLogarithmic extends Axis { if (y <= im.yorigin - im.ysize) { break; } - worker.drawLine(x0 - 1, y, x0 + 1, y, gridColor, gdef.tickStroke); - worker.drawLine(x1 - 1, y, x1 + 1, y, gridColor, gdef.tickStroke); - worker.drawLine(x0, y, x1, y, gridColor, gdef.gridStroke); + drawAxisLines.accept(y, value, gridColor); } } @@ -113,18 +157,14 @@ class ValueAxisLogarithmic extends Axis { if (im.minval < 0.0 && im.maxval > 0.0) { skipFirst = true; int y = mapper.ytr(0.0); - worker.drawLine(x0 - 2, y, x0 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x1 - 2, y, x1 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x0, y, x1, y, mGridColor, gdef.gridStroke); - String graph_label = Util.sprintf(gdef.locale, "%3.0e", 0.0); - int length = (int) (worker.getStringWidth(graph_label, font)); - worker.drawString(graph_label, x0 - length - PADDING_VLABEL, y + labelOffset, font, fontColor); + drawAxisLines.accept(y, 0.0, mGridColor); + drawAxisLabel.accept(y, 0.0); } // Draw major grid for positive values int iter = 0; - for (double value = Math.pow(10, log10(positiveMin) - - (log10(positiveMin) % log10(yloglab[majoridx][0]))); + int lasty = Integer.MAX_VALUE; + for (double value = findStart(positiveMin, majoridx); value <= im.maxval; value *= yloglab[majoridx][0]) { if (value < positiveMin) { @@ -140,19 +180,18 @@ class ValueAxisLogarithmic extends Axis { if (y <= im.yorigin - im.ysize) { break; } - worker.drawLine(x0 - 2, y, x0 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x1 - 2, y, x1 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x0, y, x1, y, mGridColor, gdef.gridStroke); - String graph_label = Util.sprintf(gdef.locale, "%3.0e", value * yloglab[majoridx][i]); - int length = (int) (worker.getStringWidth(graph_label, font)); - worker.drawString(graph_label, x0 - length - PADDING_VLABEL, y + labelOffset, font, fontColor); + // Avoid collision of labels + if ((lasty - y) > fontHeight) { + drawAxisLines.accept(y, value, mGridColor); + drawAxisLabel.accept(y, value * yloglab[majoridx][i]); + lasty = y; + } } } // Draw major grid for negative values iter = 0; - for (double value = Math.pow(10, log10(negativeMin) - - (log10(negativeMin) % log10(yloglab[majoridx][0]))); + for (double value = findStart(negativeMin, majoridx); value <= -1.0 * im.minval; value *= yloglab[majoridx][0]) { if (value < negativeMin) { @@ -168,28 +207,12 @@ class ValueAxisLogarithmic extends Axis { if (y <= im.yorigin - im.ysize) { break; } - worker.drawLine(x0 - 2, y, x0 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x1 - 2, y, x1 + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(x0, y, x1, y, mGridColor, gdef.gridStroke); - String graph_label = Util.sprintf(gdef.locale, "%3.0e", -1.0 * value * yloglab[majoridx][i]); - int length = (int) (worker.getStringWidth(graph_label, font)); - worker.drawString(graph_label, x0 - length - PADDING_VLABEL, y + labelOffset, font, fontColor); + drawAxisLines.accept(y, value, mGridColor); + drawAxisLabel.accept(y, -1.0 * value * yloglab[majoridx][i]); } } return true; } - /** - * Compute logarithm for the purposes of y-axis. - */ - static double log10(double v) { - double lv = Math.log10(Math.abs(v)); - if (lv < 0) { - // Don't cross the sign line, round to 0 if that's the case - return 0.0; - } else { - return Math.copySign(lv, v); - } - } } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisMrtg.java b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisMrtg.java index 2da2f397b..c8a2c5751 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisMrtg.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/ValueAxisMrtg.java @@ -10,15 +10,22 @@ class ValueAxisMrtg extends Axis { private final ImageWorker worker; private final RrdGraphDef gdef; - ValueAxisMrtg(RrdGraph rrdGraph) { - this(rrdGraph, rrdGraph.worker); - } - + /** + * Used for tests + * + * @param rrdGraph + * @param worker + */ ValueAxisMrtg(RrdGraph rrdGraph, ImageWorker worker) { this.im = rrdGraph.im; this.gdef = rrdGraph.gdef; this.worker = worker; - im.unit = gdef.unit; + } + + ValueAxisMrtg(RrdGraphGenerator generator) { + this.im = generator.im; + this.gdef = generator.gdef; + this.worker = generator.worker; } boolean draw() { @@ -40,14 +47,14 @@ class ValueAxisMrtg extends Axis { else { labfmt = Util.sprintf(gdef.locale, "%%4.%df", 1 - ((im.scaledstep / im.magfact > 10.0 || Math.ceil(im.scaledstep / im.magfact) == im.scaledstep / im.magfact) ? 1 : 0)); } - if (im.symbol != ' ' || im.unit != null) { + if (im.symbol != ' ' || gdef.unit != null) { labfmt += " "; } if (im.symbol != ' ') { labfmt += Character.toString(im.symbol); } - if (im.unit != null) { - labfmt += im.unit; + if (gdef.unit != null) { + labfmt += gdef.unit; } for (int i = 0; i <= 4; i++) { int y = im.yorigin - im.ysize * i / 4; @@ -55,8 +62,10 @@ class ValueAxisMrtg extends Axis { String graph_label = Util.sprintf(gdef.locale, labfmt, im.scaledstep / im.magfact * (i - im.quadrant)); int length = (int) (worker.getStringWidth(graph_label, font)); worker.drawString(graph_label, xLeft - length - PADDING_VLABEL, y + labelOffset, font, fontColor); - worker.drawLine(xLeft - 2, y, xLeft + 2, y, mGridColor, gdef.tickStroke); - worker.drawLine(xRight - 2, y, xRight + 2, y, mGridColor, gdef.tickStroke); + if (gdef.drawTicks()) { + worker.drawLine(xLeft - 2, y, xLeft + 2, y, mGridColor, gdef.tickStroke); + worker.drawLine(xRight - 2, y, xRight + 2, y, mGridColor, gdef.tickStroke); + } worker.drawLine(xLeft, y, xRight, y, mGridColor, gdef.gridStroke); } } diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ValueScaler.java b/apps/jrobin/java/src/org/rrd4j/graph/ValueScaler.java index 3b69b48fb..fc1f08226 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/ValueScaler.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/ValueScaler.java @@ -1,11 +1,6 @@ package org.rrd4j.graph; class ValueScaler { - static final String UNIT_UNKNOWN = "?"; - static final String[] UNIT_SYMBOLS = { - "a", "f", "p", "n", "µ", "m", " ", "K", "M", "G", "T", "P", "E" - }; - static final int SYMB_CENTER = 6; private final double base; private double magfact = -1; // nothing scaled before, rescale @@ -48,16 +43,11 @@ class ValueScaler { sindex = (int) (Math.floor(Math.log(Math.abs(value)) / Math.log(base))); magfact = Math.pow(base, sindex); } - if (sindex <= SYMB_CENTER && sindex >= -SYMB_CENTER) { - unit = UNIT_SYMBOLS[sindex + SYMB_CENTER]; - // I2P show 0.xxx instead of xxx m - if (unit.equals("m")) { - unit = ""; - magfact *= 1000; - } - } - else { - unit = UNIT_UNKNOWN; + unit = String.valueOf(FindUnit.resolveSymbol(sindex)); + // I2P show 0.xxx instead of xxx m + if (unit.equals("m")) { + unit = ""; + magfact *= 1000; } return new Scaled(value / magfact, unit); } diff --git a/history.txt b/history.txt index 870e1657f..10ce93585 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,7 @@ -2024-08-07 2.7.0 (API 0.9.64) released +2024-10-22 zzz + * Console: Merge in more upstream rrd4j changes for 3.10 + +2024-10-08 2.7.0 (API 0.9.64) released 2024-10-08 idk * Fix failing tests @@ -38,7 +41,7 @@ 2024-08-09 zzz * i2psnark: Reduce minimum banwdith, reduce max connections if low bandwidth - * i2psnark, susimail: Normailze strings when searching (Gitlab #488) + * i2psnark, susimail: Normalize strings when searching (Gitlab #488) * susimail: Fix searches for multiple terms * Tunnels: Do not select ElG routers for tunnels * Util: Reduce number of PRNG output buffers diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 1984cf18c..f3f107529 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -20,7 +20,7 @@ public class RouterVersion { public final static String VERSION = CoreVersion.VERSION; /** for example: "beta", "alpha", "rc" */ public final static String QUALIFIER = ""; - public final static long BUILD = 0; + public final static long BUILD = 1; /** for example "-test" */ public final static String EXTRA = ""; public final static String FULL_VERSION = VERSION + "-" + BUILD + QUALIFIER + EXTRA;