forked from I2P_Developers/i2p.i2p
Convert graphs to SVG
This commit is contained in:
@@ -37,6 +37,9 @@
|
||||
encoding="UTF-8"
|
||||
includes="**/*.java" >
|
||||
<compilerarg line="${javac.compilerargs}" />
|
||||
<classpath>
|
||||
<pathelement location="../../../core/java/build/i2p.jar" />
|
||||
</classpath>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
|
||||
431
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGGraphics2D.java
Normal file
431
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGGraphics2D.java
Normal file
@@ -0,0 +1,431 @@
|
||||
package net.i2p.rrd4j;
|
||||
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.font.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.PathIterator;
|
||||
import static java.awt.geom.PathIterator.*;
|
||||
import java.awt.image.*;
|
||||
import java.awt.image.renderable.RenderableImage;
|
||||
|
||||
/**
|
||||
* Very simple SVGGraphics2D, only enough for basic rrd4j use, without dependencies.
|
||||
* Plus a few things that rrd4j doesn't use, but not much.
|
||||
* Unsupported things will throw UnsupportedOperationExceptions.
|
||||
*
|
||||
* Supports custom RenderingHints for id and class on top-level svg element.
|
||||
* Supports custom RenderingHints for id, class, title, and arbitrary attributes
|
||||
* on all drawn elements.
|
||||
* Supports custom RenderingHints for inner SVG on all drawn elements except text.
|
||||
*
|
||||
* No standard Java AWT hints are supported.
|
||||
* Antialiasing is done automatically.
|
||||
* Antialiasing hints have no effect.
|
||||
*
|
||||
* License: Apache 2.0 (same as rrd4j)
|
||||
*
|
||||
* @since 0.9.64
|
||||
* @author zzz
|
||||
*/
|
||||
public class SimpleSVGGraphics2D extends Graphics2D {
|
||||
|
||||
//// hints - all strings except for ATTMAP ////
|
||||
|
||||
/**
|
||||
* On the top svg element.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_SVG_ID = new RHKey(1);
|
||||
/**
|
||||
* On the top svg element.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_SVG_CLASS = new RHKey(2);
|
||||
/**
|
||||
* On the top svg element.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_SVG_TITLE = new RHKey(3);
|
||||
/**
|
||||
* On the next element drawn, one-shot, will be removed after rendering.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_ELEMENT_ID = new RHKey(4);
|
||||
/**
|
||||
* On the next element drawn, one-shot, will be removed after rendering.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_ELEMENT_CLASS = new RHKey(5);
|
||||
/**
|
||||
* Value is a Map of String to String of extra attributes on the next element drawn, one-shot, will be removed after rendering.
|
||||
* Map keys must be XML-escaped by caller if necessary.
|
||||
* Map values will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_ELEMENT_ATTMAP = new RHKey(6);
|
||||
/**
|
||||
* On the next element drawn, one-shot, will be removed after rendering.
|
||||
* Value is a string and will be XML-escaped when rendering.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_ELEMENT_TITLE = new RHKey(7);
|
||||
/**
|
||||
* Put "inside" the next element drawn, one-shot, will be removed after rendering.
|
||||
* Value is an XML string and must be XML-escaped by caller if necessary.
|
||||
*/
|
||||
public static final RenderingHints.Key KEY_ELEMENT_INNERSVG = new RHKey(8);
|
||||
|
||||
private final StringBuilder buf;
|
||||
private final SimpleSVGMaker svg;
|
||||
private final Map<Object,Object> hints = new HashMap<Object,Object>();
|
||||
private AffineTransform transform = new AffineTransform();
|
||||
private final FontRenderContext frctx = new FontRenderContext(transform, true, true);
|
||||
private final int width, height;
|
||||
private final Rectangle origclip;
|
||||
// null unless different from origclip
|
||||
private Rectangle clip;
|
||||
private String clipID;
|
||||
private Color bgcolor = Color.WHITE;
|
||||
private Paint paint = Color.BLACK;
|
||||
private Stroke stroke = new BasicStroke(1);
|
||||
private Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
private boolean started;
|
||||
|
||||
public SimpleSVGGraphics2D(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
origclip = new Rectangle(0, 0, width, height);
|
||||
buf = new StringBuilder(16*1024);
|
||||
svg = new SimpleSVGMaker(buf);
|
||||
}
|
||||
|
||||
public String getSVG() {
|
||||
stop();
|
||||
String rv = buf.toString();
|
||||
dispose();
|
||||
return rv;
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (!started) {
|
||||
String id = (String) hints.remove(KEY_SVG_ID);
|
||||
String cl = (String) hints.remove(KEY_SVG_CLASS);
|
||||
svg.startSVG(width, height, bgcolor, id, cl);
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (started) {
|
||||
if (!transform.isIdentity()) {
|
||||
svg.endGroup();
|
||||
transform = frctx.getTransform();
|
||||
}
|
||||
svg.endSVG();
|
||||
clip = null;
|
||||
clipID = null;
|
||||
started = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() { buf.setLength(0); }
|
||||
|
||||
//// API bypass ////
|
||||
|
||||
/**
|
||||
* Graphics2D API bypass, advanced use only
|
||||
*/
|
||||
public SimpleSVGMaker getMaker() { start(); return svg; }
|
||||
|
||||
/**
|
||||
* Graphics2D API bypass, advanced use only
|
||||
*/
|
||||
public void append(String s) { start(); buf.append(s).append('\n'); }
|
||||
|
||||
//// draws/fills used by rrd4j ////
|
||||
|
||||
public void drawLine(int x1, int y1, int x2, int y2) {
|
||||
start();
|
||||
svg.drawLine(x1, y1, x2, y2, (Color) paint, (BasicStroke) stroke, clipID, hints);
|
||||
}
|
||||
|
||||
public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
|
||||
start();
|
||||
svg.drawPolyline(xPoints, yPoints, nPoints, (Color) paint, (BasicStroke) stroke, clipID, hints);
|
||||
}
|
||||
|
||||
public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
|
||||
start();
|
||||
String id = (String) hints.remove(KEY_ELEMENT_ID);
|
||||
String cl = (String) hints.remove(KEY_ELEMENT_CLASS);
|
||||
svg.fillPolygon(xPoints, yPoints, nPoints, (Color) paint, clipID, hints);
|
||||
}
|
||||
|
||||
public void fillRect(int x, int y, int width, int height) {
|
||||
if (!started) {
|
||||
// rrd4j calls this first with the background color, it does not call setBackground()
|
||||
if (x == 0 && y == 0 && width == this.width && height == this.height) {
|
||||
// disable setting the background color, this is it
|
||||
bgcolor = null;
|
||||
}
|
||||
start();
|
||||
}
|
||||
svg.drawRect(x, y, width, height, null, (Color) paint, null, clipID, hints);
|
||||
}
|
||||
|
||||
//// text ////
|
||||
|
||||
public void drawString(String str, int x, int y) {
|
||||
start();
|
||||
svg.drawText(str, x, y, (Color) paint, font, clipID, hints);
|
||||
}
|
||||
|
||||
public void drawString(String str, float x, float y) { drawString(str, (int) x, (int) y); }
|
||||
|
||||
public FontRenderContext getFontRenderContext() { return frctx; }
|
||||
|
||||
//// supported things not used by rrd4j ////
|
||||
|
||||
/**
|
||||
* Circles only for now, must be width == height and arcAngle == 360
|
||||
* Otherwise throws UnsupportedOperationException
|
||||
* TODO
|
||||
*/
|
||||
public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
if (width != height || arcAngle != 360)
|
||||
throw new UnsupportedOperationException("circles only!");
|
||||
start();
|
||||
int r = width / 2;
|
||||
svg.drawCircle(x + r, y + r, r, (Color) paint, null, (BasicStroke) stroke, clipID, hints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Circles only for now, must be width == height and arcAngle == 360
|
||||
* Otherwise throws UnsupportedOperationException
|
||||
* TODO
|
||||
*/
|
||||
public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
if (width != height || arcAngle != 360)
|
||||
throw new UnsupportedOperationException("circles only!");
|
||||
start();
|
||||
int r = width / 2;
|
||||
svg.drawCircle(x + r, y + r, r, null, (Color) paint, (BasicStroke) stroke, clipID, hints);
|
||||
}
|
||||
|
||||
public void clearRect(int x, int y, int width, int height) {
|
||||
boolean wasStarted = started;
|
||||
if (!wasStarted) {
|
||||
start();
|
||||
} else {
|
||||
// don't do it twice at the start
|
||||
String id = (String) hints.remove(KEY_ELEMENT_ID);
|
||||
String cl = (String) hints.remove(KEY_ELEMENT_CLASS);
|
||||
svg.drawRect(x, y, width, height, null, bgcolor, null, clipID, hints);
|
||||
}
|
||||
}
|
||||
|
||||
public void draw(Shape s) {
|
||||
drawOrFill(s, true, false);
|
||||
}
|
||||
|
||||
public void fill(Shape s) {
|
||||
drawOrFill(s, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lines only for now
|
||||
* Will draw a closed shape, open shapes will become closed.
|
||||
* Arcs will throw UnsupportedOperationException
|
||||
*/
|
||||
private void drawOrFill(Shape s, boolean draw, boolean fill) {
|
||||
int[] x = new int[16];
|
||||
int[] y = new int[16];
|
||||
int i = 0;
|
||||
float[] coords = new float[6];
|
||||
for (PathIterator it = s.getPathIterator(frctx.getTransform()); !it.isDone(); it.next()) {
|
||||
int type = it.currentSegment(coords);
|
||||
switch(type) {
|
||||
case SEG_MOVETO:
|
||||
case SEG_LINETO:
|
||||
if (i >= x.length) {
|
||||
x = Arrays.copyOf(x, x.length * 2);
|
||||
y = Arrays.copyOf(y, y.length * 2);
|
||||
}
|
||||
x[i] = (int) coords[0];
|
||||
y[i++] = (int) coords[1];
|
||||
break;
|
||||
|
||||
case SEG_CLOSE:
|
||||
break;
|
||||
|
||||
case SEG_CUBICTO:
|
||||
case SEG_QUADTO:
|
||||
throw new UnsupportedOperationException("Unsupported curved shape");
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported type " + type);
|
||||
}
|
||||
}
|
||||
if (draw)
|
||||
drawPolyline(x, y, i);
|
||||
else
|
||||
fillPolygon(x, y, i);
|
||||
}
|
||||
|
||||
//// clips ////
|
||||
|
||||
public void setClip(int x, int y, int width, int height) {
|
||||
setClip(new Rectangle(x, y, width, height));
|
||||
}
|
||||
|
||||
public void setClip(Shape clip) {
|
||||
if (clip.equals(this.clip))
|
||||
return;
|
||||
if (this.clip == null && clip.equals(origclip))
|
||||
return;
|
||||
Rectangle newclip;
|
||||
if (clip instanceof Rectangle)
|
||||
newclip = (Rectangle) clip;
|
||||
else
|
||||
newclip = clip.getBounds();
|
||||
if (clip.equals(origclip)) {
|
||||
this.clip = null;
|
||||
clipID = null;
|
||||
return;
|
||||
}
|
||||
// define new clip, save the Rectangle and ID
|
||||
clipID = svg.defineClipPath(newclip);
|
||||
this.clip = newclip;
|
||||
}
|
||||
|
||||
//// transforms ////
|
||||
|
||||
public void translate(int x, int y) { translate((double) x, (double) y); }
|
||||
|
||||
public void translate(double tx, double ty) {
|
||||
AffineTransform ntx = (AffineTransform) transform.clone();
|
||||
ntx.translate(tx, ty);
|
||||
setTransform(ntx);
|
||||
}
|
||||
|
||||
public void rotate(double theta) {
|
||||
AffineTransform ntx = (AffineTransform) transform.clone();
|
||||
ntx.rotate(theta);
|
||||
setTransform(ntx);
|
||||
}
|
||||
|
||||
public void rotate(double theta, double x, double y) {
|
||||
AffineTransform ntx = (AffineTransform) transform.clone();
|
||||
ntx.rotate(theta, x, y);
|
||||
setTransform(ntx);
|
||||
}
|
||||
|
||||
public void scale(double sx, double sy) {
|
||||
AffineTransform ntx = (AffineTransform) transform.clone();
|
||||
ntx.scale(sx, sy);
|
||||
setTransform(ntx);
|
||||
}
|
||||
|
||||
public void shear(double shx, double shy) {
|
||||
AffineTransform ntx = (AffineTransform) transform.clone();
|
||||
ntx.shear(shx, shy);
|
||||
setTransform(ntx);
|
||||
}
|
||||
|
||||
public void setTransform(AffineTransform tx) {
|
||||
// For each transform, we close the previous group if non-identity,
|
||||
// and start a new group with a transform containing the new combined transform.
|
||||
// We don't 'stack' groups with each individual transform.
|
||||
if (transform.equals(tx))
|
||||
return;
|
||||
if (!transform.isIdentity())
|
||||
svg.endGroup();
|
||||
if (!tx.isIdentity()) {
|
||||
String matrix = String.format(Locale.US, "matrix(%.3f %.3f %.3f %.3f %.3f %.3f)",
|
||||
tx.getScaleX(), tx.getShearY(),
|
||||
tx.getShearX(), tx.getScaleY(),
|
||||
tx.getTranslateX(), tx.getTranslateY());
|
||||
svg.startGroup(null, null, "transform", matrix);
|
||||
}
|
||||
transform = tx;
|
||||
}
|
||||
|
||||
public AffineTransform getTransform() { return transform; }
|
||||
|
||||
//// setters ////
|
||||
|
||||
public void setFont(Font font) { this.font = font; }
|
||||
public void setPaint(Paint paint) { this.paint = paint; }
|
||||
public void setStroke(Stroke stroke) { this.stroke = stroke; }
|
||||
|
||||
//// we support these but unused by rrd4j ////
|
||||
|
||||
public void setBackground(Color color) { bgcolor = color; }
|
||||
public Color getBackground() { return bgcolor; }
|
||||
public Shape getClip() { return clip; }
|
||||
public Rectangle getClipBounds() { return clip; }
|
||||
public void setColor(Color color) { paint = color; }
|
||||
public Color getColor() { return (Color) paint; }
|
||||
public Font getFont() { return font; }
|
||||
public Paint getPaint() { return paint; }
|
||||
public Stroke getStroke() { return stroke; }
|
||||
|
||||
//// Hints ////
|
||||
|
||||
private static class RHKey extends RenderingHints.Key {
|
||||
public RHKey(int k) {
|
||||
super(k);
|
||||
}
|
||||
|
||||
public boolean isCompatibleValue(Object o) {
|
||||
if (intKey() == 6)
|
||||
return o instanceof Map;
|
||||
return o instanceof String;
|
||||
}
|
||||
}
|
||||
|
||||
public void addRenderingHints(Map<?,?> hints) { this.hints.putAll(hints); }
|
||||
public Object getRenderingHint(RenderingHints.Key hintKey) { return hints.get(hintKey); }
|
||||
public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { hints.put(hintKey, hintValue); }
|
||||
public void setRenderingHints(Map<?,?> hints) { this.hints.clear(); addRenderingHints(hints); }
|
||||
|
||||
//// unsupported things ////
|
||||
//// all do nothing or throw ////
|
||||
|
||||
public void clipRect(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
|
||||
public void clip(Shape s) { throw new UnsupportedOperationException(); }
|
||||
public void copyArea(int x, int y, int width, int height, int dx, int dy) { throw new UnsupportedOperationException(); }
|
||||
public Graphics create() { throw new UnsupportedOperationException(); }
|
||||
public void drawGlyphVector(GlyphVector g, float x, float y) { throw new UnsupportedOperationException(); }
|
||||
public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int x, int y, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
|
||||
public void drawOval(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
|
||||
public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { throw new UnsupportedOperationException(); }
|
||||
public void drawRenderableImage(RenderableImage img, AffineTransform xform) { throw new UnsupportedOperationException(); }
|
||||
public void drawRenderedImage(RenderedImage img, AffineTransform xform) { throw new UnsupportedOperationException(); }
|
||||
public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { throw new UnsupportedOperationException(); }
|
||||
public void drawString(AttributedCharacterIterator iterator, float x, float y) { throw new UnsupportedOperationException(); }
|
||||
public void drawString(AttributedCharacterIterator iterator, int x, int y) { throw new UnsupportedOperationException(); }
|
||||
public void fillOval(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
|
||||
public void fillPolyline(int[] xPoints, int[] yPoints, int nPoints) { throw new UnsupportedOperationException(); }
|
||||
public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { throw new UnsupportedOperationException(); }
|
||||
public Composite getComposite() { return null; }
|
||||
public GraphicsConfiguration getDeviceConfiguration() { return null; }
|
||||
public FontMetrics getFontMetrics(Font f) { return null; }
|
||||
public RenderingHints getRenderingHints() { return null; }
|
||||
public boolean hit(Rectangle rect, Shape s, boolean onStroke) { return false; }
|
||||
public void setComposite(Composite comp) { throw new UnsupportedOperationException(); }
|
||||
public void setPaintMode() {}
|
||||
public void setXORMode(Color color) { throw new UnsupportedOperationException(); }
|
||||
public void transform(AffineTransform tx) {}
|
||||
}
|
||||
75
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGImageWorker.java
Normal file
75
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGImageWorker.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package net.i2p.rrd4j;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.rrd4j.graph.ImageWorker;
|
||||
|
||||
/**
|
||||
* rrd4j adapter for SimpleSVGGraphics2D
|
||||
*
|
||||
* Requires: rrd4j 3.10 or higher
|
||||
* Ref: https://github.com/rrd4j/rrd4j/issues/165
|
||||
*
|
||||
* Usage:
|
||||
* No ImageIO/BufferedImage/ImageWriter required!
|
||||
*
|
||||
*<pre>
|
||||
* RRDGraph graph = new RrdGraph(graphdef, new SimpleSVGImageWorker(width, height));
|
||||
* outputstream.write(graph.getRrdGraphInfo().getBytes());
|
||||
*</pre>
|
||||
*
|
||||
* License: Apache 2.0 (same as rrd4j)
|
||||
*
|
||||
* @since 0.9.64
|
||||
* @author zzz
|
||||
*/
|
||||
public class SimpleSVGImageWorker extends ImageWorker {
|
||||
private SimpleSVGGraphics2D g2d;
|
||||
private AffineTransform initialAffineTransform;
|
||||
private int imgWidth;
|
||||
private int imgHeight;
|
||||
|
||||
public SimpleSVGImageWorker(int width, int height) {
|
||||
resize(width, height);
|
||||
}
|
||||
|
||||
protected void resize(int width, int height) {
|
||||
imgWidth = width;
|
||||
imgHeight = height;
|
||||
g2d = new SimpleSVGGraphics2D(imgWidth, imgHeight);
|
||||
initialAffineTransform = g2d.getTransform();
|
||||
setG2d(g2d);
|
||||
}
|
||||
|
||||
protected void reset(Graphics2D g2d) {
|
||||
g2d.setTransform(initialAffineTransform);
|
||||
g2d.setClip(0, 0, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
protected void makeImage(OutputStream os) throws IOException {
|
||||
os.write(g2d.getSVG().getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden because the SVG format essentially strips leading/trailing spaces,
|
||||
* causing alignment issues in ValueAxis with the %x.y number formatting.
|
||||
* Consecutive spaces within text are also probably collapsed, that is not addressed here.
|
||||
*/
|
||||
@Override
|
||||
protected void drawString(String text, int x, int y, Font font, Paint paint) {
|
||||
super.drawString(text.trim(), x, y, font, paint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden because the SVG format essentially strips leading/trailing spaces,
|
||||
* causing alignment issues in ValueAxis with the %x.y number formatting.
|
||||
* Consecutive spaces within text are also probably collapsed, that is not addressed here.
|
||||
*/
|
||||
@Override
|
||||
protected double getStringWidth(String text, Font font) {
|
||||
return super.getStringWidth(text.trim(), font);
|
||||
}
|
||||
}
|
||||
453
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGMaker.java
Normal file
453
apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGMaker.java
Normal file
@@ -0,0 +1,453 @@
|
||||
package net.i2p.rrd4j;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
import net.i2p.data.DataHelper;
|
||||
import static net.i2p.rrd4j.SimpleSVGGraphics2D.*;
|
||||
|
||||
/**
|
||||
* Create full or partial SVG images, without dependencies.
|
||||
* Does not extend or use Graphics2D or ImageWriter.
|
||||
*
|
||||
* Each drawn element can be passed an optional CSS ID and/or classes,
|
||||
* for easy styling and manipulation via CSS or js.
|
||||
* All parameters are set as attributes, not as inline style,
|
||||
* so a separate CSS style may easily override them.
|
||||
* If inline style is desired, add it with the KEY_ELEMENT_ATTMAP hint.
|
||||
*
|
||||
* Unlike in Graphics2D, the border and fill for an object may be drawn in
|
||||
* the same call, with separate colors.
|
||||
*
|
||||
* There is no state here other than the StringBuffer;
|
||||
* there is no concept of current Color or Stroke or Font;
|
||||
* caller must keep track of current Colors, Stroke, and Font, and pass them in
|
||||
* on every draw() call, and/or overridden via CSS.
|
||||
*
|
||||
* License: Apache 2.0 (same as rrd4j)
|
||||
*
|
||||
* @since 0.9.64
|
||||
* @author zzz
|
||||
*/
|
||||
public class SimpleSVGMaker {
|
||||
|
||||
private final StringBuilder buf;
|
||||
private int clipid;
|
||||
|
||||
public SimpleSVGMaker(StringBuilder buf) {
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start svg tag
|
||||
* @param bgcolor null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void startSVG(int width, int height, Color bgcolor, String id, String clz) {
|
||||
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
|
||||
// "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n" +
|
||||
"<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" ");
|
||||
addIDClass(id, clz);
|
||||
addString("viewBox", "0 0 " + width + ' ' + height);
|
||||
addInt("width", width);
|
||||
addInt("height", height);
|
||||
buf.append(">\n");
|
||||
if (bgcolor != null && bgcolor.getAlpha() > 0)
|
||||
drawRect(0, 0, width, height, null, bgcolor, null, null, Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* End svg tag
|
||||
*/
|
||||
public void endSVG() {
|
||||
buf.append("</svg>\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start group
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
* @param att an attribute to add att=val, e.g. "transform", or null for none
|
||||
* @param val an attribute to add att=val, or null for none
|
||||
*/
|
||||
public void startGroup(String id, String clz, String att, String val) {
|
||||
buf.append("<g ");
|
||||
addIDClass(id, clz);
|
||||
if (att != null && val != null)
|
||||
addString(att, val);
|
||||
buf.append(">\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* End group
|
||||
*/
|
||||
public void endGroup() {
|
||||
buf.append("</g>\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Define clip path
|
||||
* @return a unique ID to pass to draw() calls
|
||||
*/
|
||||
public String defineClipPath(Rectangle clip) {
|
||||
buf.append("<clipPath ");
|
||||
String rv = "clip-" + hashCode() + '-' + (clipid++);
|
||||
addIDClass(rv, null);
|
||||
buf.append("><rect ");
|
||||
addInt("x", (int) clip.getX());
|
||||
addInt("y", (int) clip.getY());
|
||||
addInt("width", (int) clip.getWidth());
|
||||
addInt("height", (int) clip.getHeight());
|
||||
buf.append("/></clipPath>\n");
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw circle centered on x,y with a radius given
|
||||
* @param border null for none
|
||||
* @param fill null for none
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawCircle(int x, int y, int radius, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
|
||||
buf.append("<circle ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
addInt("cx", x);
|
||||
addInt("cy", y);
|
||||
addInt("r", radius);
|
||||
addStroke("fill", fill, null);
|
||||
addStroke("stroke", border, stroke);
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
|
||||
if (title != null || inner != null) {
|
||||
buf.append(">\n");
|
||||
addInner(title, inner);
|
||||
buf.append("</circle>\n");
|
||||
} else {
|
||||
buf.append("/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw square centered on x,y with a width/height given
|
||||
* @param border null for none
|
||||
* @param fill null for none
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawSquare(int x, int y, int sz, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
|
||||
drawRect(x - (sz/2), y - (sz/2), sz, sz, border, fill, stroke, clipid, hints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rect
|
||||
* @param border null for none
|
||||
* @param fill null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawRect(int x, int y, int width, int height, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
|
||||
buf.append("<rect ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
addInt("x", x);
|
||||
addInt("y", y);
|
||||
addInt("width", width);
|
||||
addInt("height", height);
|
||||
addStroke("fill", fill, null);
|
||||
addStroke("stroke", border, stroke);
|
||||
buf.append("shape-rendering=\"crispEdges\" ");
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
|
||||
if (title != null || inner != null) {
|
||||
buf.append(">\n");
|
||||
addInner(title, inner);
|
||||
buf.append("</rect>\n");
|
||||
} else {
|
||||
buf.append("/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw line
|
||||
* @param color null to let CSS do it
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawLine(int x1, int y1, int x2, int y2, Color color, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
|
||||
buf.append("<line ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
addInt("x1", x1);
|
||||
addInt("y1", y1);
|
||||
addInt("x2", x2);
|
||||
addInt("y2", y2);
|
||||
addStroke("stroke", color, stroke);
|
||||
// don't do this for diagonal lines, it kills antialiasing
|
||||
if (x1 == x2 || y1 == y2)
|
||||
buf.append("shape-rendering=\"crispEdges\" ");
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
|
||||
if (title != null || inner != null) {
|
||||
buf.append(">\n");
|
||||
addInner(title, inner);
|
||||
buf.append("</line>\n");
|
||||
} else {
|
||||
buf.append("/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw polyline
|
||||
* @param color null to let CSS do it
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawPolyline(int[] x, int[] y, int sz, Color color, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
|
||||
if (sz < 2)
|
||||
return;
|
||||
buf.append("<path ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
buf.append("d=\"M");
|
||||
buf.append(x[0]).append(',').append(y[0]);
|
||||
for (int i = 1; i < sz; i++) {
|
||||
// use relative coords to save a little space
|
||||
buf.append('l');
|
||||
buf.append(x[i] - x[i-1]).append(',').append(y[i] - y[i-1]);
|
||||
}
|
||||
buf.append("\" ");
|
||||
addStroke("stroke", color, stroke);
|
||||
buf.append("fill=\"none\" ");
|
||||
// this is good for the horizontal/vertical paths drawn by rrd4j,
|
||||
// but not so great for diagonal path segments
|
||||
// Take our cue from the first segment
|
||||
if (x[0] == x[1] || y[0] == y[1])
|
||||
buf.append("shape-rendering=\"crispEdges\" ");
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
|
||||
if (title != null || inner != null) {
|
||||
buf.append(">\n");
|
||||
addInner(title, inner);
|
||||
buf.append("</path>\n");
|
||||
} else {
|
||||
buf.append("/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill polygon
|
||||
* @param color null to let CSS do it
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void fillPolygon(int[] x, int[] y, int sz, Color color, String clipid, Map<Object, Object> hints) {
|
||||
if (sz < 2)
|
||||
return;
|
||||
buf.append("<path ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
buf.append("d=\"M");
|
||||
buf.append(x[0]).append(',').append(y[0]);
|
||||
for (int i = 1; i < sz; i++) {
|
||||
// use relative coords to save a little space
|
||||
buf.append('l');
|
||||
buf.append(x[i] - x[i-1]).append(',').append(y[i] - y[i-1]);
|
||||
}
|
||||
buf.append("Z\" ");
|
||||
addStroke("fill", color, null);
|
||||
buf.append("stroke=\"none\" ");
|
||||
// see above
|
||||
if (x[0] == x[1] || y[0] == y[1])
|
||||
buf.append("shape-rendering=\"crispEdges\" ");
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
|
||||
if (title != null || inner != null) {
|
||||
buf.append(">\n");
|
||||
addInner(title, inner);
|
||||
buf.append("</path>\n");
|
||||
} else {
|
||||
buf.append("/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text
|
||||
* @param color null to let CSS do it
|
||||
* @param font null to let CSS do it
|
||||
* @param clipid as returned from defineClipID() or null for none
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
public void drawText(String text, int x, int y, Color color, Font font, String clipid, Map<Object, Object> hints) {
|
||||
buf.append("<text ");
|
||||
addAttributes(hints);
|
||||
addClipPath(clipid);
|
||||
addInt("x", x);
|
||||
addInt("y", y);
|
||||
addStroke("fill", color, null);
|
||||
if (font != null) {
|
||||
addString("font-family", font.getFamily());
|
||||
buf.append("font-size=\"").append(font.getSize()).append("px\" ");
|
||||
if (font.isBold())
|
||||
buf.append("font-weight=\"bold\" ");
|
||||
if (font.isItalic())
|
||||
buf.append("font-style=\"italic\" ");
|
||||
}
|
||||
buf.append("text-rendering=\"optimizeLegibility\">").append(DataHelper.escapeHTML(text));
|
||||
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
|
||||
if (title != null)
|
||||
addInner(title, null);
|
||||
buf.append("</text>\n");
|
||||
}
|
||||
|
||||
private void addInt(String key, int val) {
|
||||
buf.append(key).append("=\"").append(val).append("\" ");
|
||||
}
|
||||
|
||||
private void addString(String key, String val) {
|
||||
buf.append(key).append("=\"").append(DataHelper.escapeHTML(val)).append("\" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id CSS id or null for none
|
||||
* @param clz CSS class or null for none
|
||||
*/
|
||||
private void addIDClass(String id, String clz) {
|
||||
if (id != null)
|
||||
addString("id", id);
|
||||
if (clz != null)
|
||||
addString("class", clz);
|
||||
}
|
||||
|
||||
private void addAttributes(Map<Object, Object> hints) {
|
||||
String id = (String) hints.remove(KEY_ELEMENT_ID);
|
||||
if (id != null)
|
||||
addString("id", id);
|
||||
String clz = (String) hints.remove(KEY_ELEMENT_CLASS);
|
||||
if (clz != null)
|
||||
addString("class", clz);
|
||||
Map<?,?> atts = (Map) hints.remove(KEY_ELEMENT_ATTMAP);
|
||||
if (atts != null) {
|
||||
for (Map.Entry e : atts.entrySet()) {
|
||||
addString((String) e.getKey(), (String) e.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type "fill" or "stroke"
|
||||
* @param color null to let CSS do it
|
||||
* @param stroke null to omit for fill
|
||||
*/
|
||||
private void addStroke(String type, Color color, BasicStroke stroke) {
|
||||
buf.append(type);
|
||||
if (color != null) {
|
||||
// todo getRGB() #tohex & 0xffffff
|
||||
//buf.append("=\"rgb(").append(color.getRed())
|
||||
// .append(',').append(color.getGreen())
|
||||
// .append(',').append(color.getBlue())
|
||||
// .append(")\" ");
|
||||
buf.append("=\"#").append(String.format(Locale.US, "%06x", color.getRGB() & 0xffffff))
|
||||
.append("\" ");
|
||||
int alpha = color.getAlpha();
|
||||
if (alpha < 255) {
|
||||
buf.append(type).append("-opacity=\"")
|
||||
.append(String.format(Locale.US, "%.2f", alpha / 255f))
|
||||
.append("\" ");
|
||||
}
|
||||
} else {
|
||||
// default is black opaque, so fixup for none
|
||||
buf.append("=\"none\" ");
|
||||
}
|
||||
if (stroke != null) {
|
||||
int width = (int) stroke.getLineWidth();
|
||||
if (width > 0) {
|
||||
if (width != 1)
|
||||
buf.append(type).append("-width=\"").append(width).append("\" ");
|
||||
float[] dash = stroke.getDashArray();
|
||||
if (dash != null && dash.length > 1) {
|
||||
buf.append("stroke-dasharray=\"");
|
||||
for (int i = 0; i < dash.length; i++) {
|
||||
buf.append((int) dash[i]);
|
||||
if (i != dash.length - 1)
|
||||
buf.append(' ');
|
||||
}
|
||||
buf.append("\" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clipid as received from defineClipPath() or null for none
|
||||
*/
|
||||
private void addClipPath(String clipid) {
|
||||
if (clipid != null)
|
||||
buf.append("clip-path='url(#").append(clipid).append(")' ");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param title, will be XML escaped here, or null
|
||||
* @param other full elements, must be XML escaped, or null
|
||||
*/
|
||||
private void addInner(String title, String inner) {
|
||||
if (title != null)
|
||||
buf.append(" <title>").append(DataHelper.escapeHTML(title)).append("</title>\n");
|
||||
if (inner != null)
|
||||
buf.append(" ").append(inner).append("\n");
|
||||
}
|
||||
|
||||
/*
|
||||
public void main(String[] args) {
|
||||
StringBuilder buf = new StringBuilder(2048);
|
||||
SimpleSVGMaker g = new SimpleSVGMaker(buf);
|
||||
Font f = new Font("Dialog", Font.BOLD, 24);
|
||||
Color c = new Color(255, 128, 128);
|
||||
g.startSVG(190, 200, c, "id", "class");
|
||||
g.startGroup("gid", "class", "transform", "matrix");
|
||||
c = new Color(255, 0, 0);
|
||||
BasicStroke s = new BasicStroke(4);
|
||||
Map<Object,Object> hints = new java.util.HashMap<Object,Object>();
|
||||
g.drawSquare(100, 36, 17, null, c, s, null, hints);
|
||||
c = new Color(33, 33, 33, 128);
|
||||
s = new BasicStroke(8);
|
||||
g.drawCircle(75, 56, 27, c, null, s, null, hints);
|
||||
g.drawCircle(100, 100, 110, c, null, s, null, hints);
|
||||
c = new Color(0, 255, 0);
|
||||
s = new BasicStroke(2);
|
||||
g.drawLine(55, 96, 97, 178, c, s, null, hints);
|
||||
int[] xx = { 10, 20, 30, 40, 150 };
|
||||
int[] yy = { 81, 92, 113, 184, 29 };
|
||||
c = new Color(0, 0, 255);
|
||||
s = new BasicStroke(2);
|
||||
g.drawPolyline(xx, yy, 5, c, s, null, hints);
|
||||
Color cc = new Color(128, 128, 0, 128);
|
||||
Color ccc = new Color(128, 0, 192, 128);
|
||||
g.drawRect(100, 80, 40, 20, cc, ccc, s, null, hints);
|
||||
c = new Color(0, 128, 128);
|
||||
g.drawText("foo", 135, 156, c, f, null, hints);
|
||||
c = new Color(128, 128, 0);
|
||||
f = new Font(Font.SANS_SERIF, Font.ITALIC, 20);
|
||||
g.drawText("bar", 115, 136, c, f, null, hints);
|
||||
f = new Font(Font.SANS_SERIF, Font.PLAIN, 16);
|
||||
g.drawText("baz", 115, 176, c, f, null, hints);
|
||||
g.endGroup();
|
||||
g.endSVG();
|
||||
System.out.print(buf.toString());
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.rrd4j.SimpleSVGImageWorker;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.util.EventLog;
|
||||
import static net.i2p.router.web.GraphConstants.*;
|
||||
@@ -374,7 +375,8 @@ class SummaryRenderer {
|
||||
RrdGraph graph;
|
||||
try {
|
||||
// NPE here if system is missing fonts - see ticket #915
|
||||
graph = new RrdGraph(def);
|
||||
SimpleSVGImageWorker svg = new SimpleSVGImageWorker(width, height);
|
||||
graph = new RrdGraph(def, svg);
|
||||
} catch (NullPointerException npe) {
|
||||
_log.error("Error rendering graph", npe);
|
||||
StatSummarizer.setDisabled(_context);
|
||||
@@ -385,13 +387,8 @@ class SummaryRenderer {
|
||||
StatSummarizer.setDisabled(_context);
|
||||
throw new IOException("Error rendering - disabling graph generation. Missing font?");
|
||||
}
|
||||
int totalWidth = graph.getRrdGraphInfo().getWidth();
|
||||
int totalHeight = graph.getRrdGraphInfo().getHeight();
|
||||
BufferedImage img = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_USHORT_565_RGB);
|
||||
Graphics gfx = img.getGraphics();
|
||||
graph.render(gfx);
|
||||
ios = new MemoryCacheImageOutputStream(out);
|
||||
ImageIO.write(img, "png", ios);
|
||||
out.write(graph.getRrdGraphInfo().getBytes());
|
||||
out.flush();
|
||||
|
||||
_context.statManager().addRateData("graph.renderTime", System.currentTimeMillis() - begin);
|
||||
} catch (RrdException re) {
|
||||
|
||||
@@ -53,8 +53,9 @@ if ( !rendered && ((rs != null) || fakeBw) ) {
|
||||
rendered = ss.getXML(rate, cout);
|
||||
}
|
||||
} else {
|
||||
response.setContentType("image/png");
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + stat + ".png\"");
|
||||
response.setContentType("image/svg+xml");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + stat + ".svg\"");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
// http://jira.codehaus.org/browse/JETTY-1346
|
||||
|
||||
Reference in New Issue
Block a user