Skip to content

Commit 29ea435

Browse files
committed
initial version of svg shape analyzer
Signed-off-by: Stefan Niederhauser <[email protected]>
1 parent b84cfa7 commit 29ea435

15 files changed

+424
-71
lines changed

graphviz-java/src/main/java/guru/nidi/graphviz/engine/EngineResult.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
import javax.annotation.Nullable;
1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.nio.file.Files;
2122
import java.util.Objects;
2223
import java.util.function.Consumer;
2324
import java.util.function.Function;
25+
import java.util.stream.Stream;
26+
27+
import static java.nio.charset.StandardCharsets.UTF_8;
2428

2529
public final class EngineResult {
2630
@Nullable
@@ -60,6 +64,10 @@ public <T> T map(Function<File, T> fileMapper, Function<String, T> stringMapper)
6064
return res;
6165
}
6266

67+
public String asString() throws IOException {
68+
return mapIO(EngineResult::readFile, string -> string);
69+
}
70+
6371
<T> T mapIO(IOFunction<File, T> fileMapper, IOFunction<String, T> stringMapper) throws IOException {
6472
final T res = string == null ? fileMapper.apply(file) : stringMapper.apply(string);
6573
close();
@@ -72,6 +80,14 @@ private void close() {
7280
}
7381
}
7482

83+
private static String readFile(File file) throws IOException {
84+
final StringBuilder s = new StringBuilder();
85+
try (Stream<String> stream = Files.lines(file.toPath(), UTF_8)) {
86+
stream.forEach(line -> s.append(line).append("\n"));
87+
}
88+
return s.toString();
89+
}
90+
7591
@Override
7692
public boolean equals(Object o) {
7793
if (this == o) {

graphviz-java/src/main/java/guru/nidi/graphviz/engine/SvgSizeAdjuster.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package guru.nidi.graphviz.engine;
1717

18+
import guru.nidi.graphviz.model.SvgSizeAnalyzer;
1819
import org.slf4j.Logger;
1920
import org.slf4j.LoggerFactory;
2021

@@ -44,10 +45,10 @@ private static String withoutPrefix(String svg) {
4445

4546
private static String pointsToPixels(String svg, double dpi, int width, int height, double scale) {
4647
try {
47-
final SvgSizeAnalyzer analyzer = new SvgSizeAnalyzer(svg);
48+
final SvgSizeAnalyzer analyzer = SvgSizeAnalyzer.svg(svg);
4849
setSize(analyzer, width, height, scale);
4950
setScale(analyzer, dpi);
50-
return analyzer.adjusted();
51+
return analyzer.getSvg();
5152
} catch (IllegalArgumentException e) {
5253
LOG.warn(e.getMessage());
5354
return svg;

graphviz-java/src/main/java/guru/nidi/graphviz/model/GraphElementFinder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.function.Consumer;
2324

2425
import static java.util.stream.Collectors.toMap;
2526

@@ -28,6 +29,12 @@ public class GraphElementFinder extends SvgElementFinder {
2829
private final Map<String, Link> links;
2930
private final Map<String, MutableGraph> graphs;
3031

32+
public static String use(String svg, Graph g, Consumer<GraphElementFinder> actions) {
33+
final GraphElementFinder finder = new SvgElementFinder(svg).fromGraph(g);
34+
actions.accept(finder);
35+
return finder.getSvg();
36+
}
37+
3138
GraphElementFinder(SvgElementFinder finder, MutableGraph graph) {
3239
super(finder);
3340
nodes = graph.nodes().stream().collect(toMap(n -> name(n), n -> n));

graphviz-java/src/main/java/guru/nidi/graphviz/model/SvgElementFinder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class SvgElementFinder {
4646
EXPR_NODE = pathExpression(X_PATH, "//g[contains(@class,'node')]"),
4747
EXPR_EDGE = pathExpression(X_PATH, "//g[contains(@class,'edge')]"),
4848
EXPR_CLUSTER = pathExpression(X_PATH, "//g[contains(@class,'cluster')]");
49-
private final Document doc;
49+
protected final Document doc;
5050
private final boolean hasHeader;
5151

5252
public static String use(String svg, Consumer<SvgElementFinder> actions) {
@@ -120,7 +120,7 @@ public List<Element> findLinks() {
120120

121121
public static List<String> linkedNodeNamesOf(Element e) {
122122
final String name = e.getElementsByTagName("title").item(0).getTextContent();
123-
return asList(name.split("--"));
123+
return asList(name.split(name.contains("--") ? "--" : "->"));
124124
}
125125

126126
@Nullable
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright © 2015 Stefan Niederhauser ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package guru.nidi.graphviz.model;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.w3c.dom.Element;
21+
import org.w3c.dom.NodeList;
22+
23+
import javax.annotation.Nullable;
24+
import java.awt.*;
25+
26+
public class SvgShapeAnalyzer extends SvgElementFinder {
27+
private static final Logger LOG = LoggerFactory.getLogger(SvgShapeAnalyzer.class);
28+
29+
private final double xFactor;
30+
private final double yFactor;
31+
private final SvgSizeAnalyzer transform;
32+
33+
public SvgShapeAnalyzer(SvgElementFinder finder, int width, int height) {
34+
super(finder);
35+
final String viewBox = finder.doc.getDocumentElement().getAttribute("viewBox");
36+
final String[] viewBoxParts = viewBox.split(" ");
37+
xFactor = width / Double.parseDouble(viewBoxParts[2]);
38+
yFactor = height / Double.parseDouble(viewBoxParts[3]);
39+
final Element g = (Element) finder.doc.getDocumentElement().getElementsByTagName("g").item(0);
40+
transform = SvgSizeAnalyzer.transform(g.getAttribute("transform"));
41+
}
42+
43+
@Nullable
44+
public Rectangle getBoundingBox(Element e) {
45+
if (!e.getAttribute("class").contains("node")) {
46+
LOG.error("Currently only nodes are supported.");
47+
return null;
48+
}
49+
final NodeList ellipses = e.getElementsByTagName("ellipse");
50+
if (ellipses.getLength() == 0) {
51+
LOG.error("Currently only nodes with shape ellipse supported.");
52+
return null;
53+
}
54+
final Element ellipse = (Element) ellipses.item(0);
55+
final double cx = xFactor * (numAttr(ellipse, "cx") + transform.getTranslateX());
56+
final double cy = yFactor * (numAttr(ellipse, "cy") + transform.getTranslateY());
57+
final double rx = xFactor * numAttr(ellipse, "rx");
58+
final double ry = yFactor * numAttr(ellipse, "ry");
59+
return new Rectangle((int) Math.round(cx - rx), (int) Math.round(cy - ry),
60+
(int) Math.round(2 * rx), (int) Math.round(2 * ry));
61+
}
62+
63+
private double numAttr(Element e, String attr) {
64+
return Double.parseDouble(e.getAttribute(attr));
65+
}
66+
}

graphviz-java/src/main/java/guru/nidi/graphviz/engine/SvgSizeAnalyzer.java renamed to graphviz-java/src/main/java/guru/nidi/graphviz/model/SvgSizeAnalyzer.java

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,24 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package guru.nidi.graphviz.engine;
16+
package guru.nidi.graphviz.model;
1717

1818
import javax.annotation.Nullable;
1919
import java.util.regex.Matcher;
2020
import java.util.regex.Pattern;
2121

2222
import static java.util.regex.Pattern.DOTALL;
2323

24-
class SvgSizeAnalyzer {
24+
public final class SvgSizeAnalyzer {
25+
private static final Pattern TRANSFORM_PATTERN = Pattern.compile(
26+
"scale\\((?<scaleX>[0-9.]+) (?<scaleY>[0-9.]+)\\) "
27+
+ "rotate\\((?<rotate>[0-9.]+)\\) "
28+
+ "translate\\((?<translateX>[0-9.]+) (?<translateY>[0-9.]+)\\)",
29+
DOTALL);
2530
private static final Pattern SVG_PATTERN = Pattern.compile(
2631
"<svg width=\"(?<width>\\d+)(?<unit>p[tx])\" height=\"(?<height>\\d+)p[tx]\""
2732
+ "(?<between>.*?>\\R<g.*?)transform=\""
28-
+ "scale\\((?<scaleX>[0-9.]+) (?<scaleY>[0-9.]+)\\) "
29-
+ "rotate\\((?<rotate>[0-9.]+)\\) "
30-
+ "translate\\((?<translateX>[0-9.]+) (?<translateY>[0-9.]+)\\)",
33+
+ TRANSFORM_PATTERN.pattern(),
3134
DOTALL);
3235
private final Matcher matcher;
3336
@Nullable
@@ -38,26 +41,48 @@ class SvgSizeAnalyzer {
3841
private Double scaleX;
3942
@Nullable
4043
private Double scaleY;
44+
@Nullable
45+
private Double rotate;
46+
@Nullable
47+
private Double translateX;
48+
@Nullable
49+
private Double translateY;
50+
51+
public static SvgSizeAnalyzer svg(String svg) {
52+
return new SvgSizeAnalyzer(SVG_PATTERN, svg);
53+
}
54+
55+
public static SvgSizeAnalyzer transform(String transform) {
56+
return new SvgSizeAnalyzer(TRANSFORM_PATTERN, transform);
57+
}
4158

42-
SvgSizeAnalyzer(String svg) {
43-
matcher = SVG_PATTERN.matcher(svg);
59+
private SvgSizeAnalyzer(Pattern pattern, String input) {
60+
matcher = pattern.matcher(input);
4461
if (!matcher.find()) {
4562
throw new IllegalArgumentException("Generated SVG has not the expected format. "
4663
+ "There might be image size problems.");
4764
}
4865
}
4966

50-
public String adjusted() {
67+
public String getSvg() {
5168
final String size = width == null
5269
? "width=\"" + getWidth() + getUnit() + "\" height=\"" + getHeight() + getUnit() + "\""
5370
: "width=\"" + width + "px\" height=\"" + height + "px\"";
71+
return matcher.replaceFirst("<svg " + size + matcher.group("between")
72+
+ "transform=\"" + getTransform());
73+
}
74+
75+
public String getTransform() {
5476
final String scale = scaleX == null
5577
? "scale(" + getScaleX() + " " + getScaleY() + ") "
5678
: "scale(" + scaleX + " " + scaleY + ") ";
57-
final String rotate = "rotate(" + getRotate() + ") ";
58-
final String translate = "translate(" + getTranslateX() + " " + getTranslateY() + ")";
59-
return matcher.replaceFirst("<svg " + size + matcher.group("between") +
60-
"transform=\"" + scale + rotate + translate);
79+
final String rot = rotate == null
80+
? "rotate(" + getRotate() + ") "
81+
: "rotate(" + rotate + ") ";
82+
final String translate = translateX == null
83+
? "translate(" + getTranslateX() + " " + getTranslateY() + ")"
84+
: "translate(" + translateX + " " + translateY + ")";
85+
return scale + rot + translate;
6186
}
6287

6388
public int getWidth() {
@@ -101,4 +126,13 @@ public void setScale(double scaleX, double scaleY) {
101126
this.scaleX = scaleX;
102127
this.scaleY = scaleY;
103128
}
129+
130+
public void setRotate(double rotate) {
131+
this.rotate = rotate;
132+
}
133+
134+
public void setTranslate(double translateX, double translateY) {
135+
this.translateX = translateX;
136+
this.translateY = translateY;
137+
}
104138
}

graphviz-java/src/test/java/guru/nidi/graphviz/CodeAnalysisTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ protected PmdResult analyzePmd() {
110110
.because("it's ok here",
111111
In.loc("Rasterizer#getDefault").ignore("CompareObjectsWithEquals"),
112112
In.locs("Format", "AttributeConfigs").ignore("AvoidDuplicateLiterals"),
113-
In.locs("LabelTest", "RankTest", "*DatatypeTest", "AttributeValidatorTest", "ParserTest", "JavascriptEngineTest", "GraphvizServerTest", "SvgElementFinderTest")
113+
In.locs("LabelTest", "RankTest", "*DatatypeTest", "AttributeValidatorTest", "ParserTest", "JavascriptEngineTest", "GraphvizServerTest", "SvgElementFinderTest", "SvgSizeAnalyzerTest")
114114
.ignore("JUnitTestContainsTooManyAsserts"),
115115
In.locs("DatatypeTest").ignore("TestClassWithoutTestCases"),
116116
In.loc("SerializerImpl").ignore("AvoidStringBufferField", "CompareObjectsWithEquals"),
117-
In.locs("ThrowingFunction", "GraphvizLoader", "GraphvizServerTest").ignore("AvoidThrowingRawExceptionTypes", "AvoidCatchingGenericException"),
117+
In.locs("ThrowingFunction", "GraphvizLoader", "GraphvizServerTest", "GraphvizPanel").ignore("AvoidThrowingRawExceptionTypes", "AvoidCatchingGenericException"),
118118
In.locs("GraphvizServer", "SerializerImpl").ignore("AvoidInstantiatingObjectsInLoops"),
119119
In.clazz(Shape.class).ignore("AvoidFieldNameMatchingTypeName"),
120120
In.locs("CommandRunnerTest", "EngineResultTest", "GraphvizServerTest").ignore("JUnitTestsShouldIncludeAssert"),
@@ -160,7 +160,7 @@ protected CpdResult analyzeCpd() {
160160
protected CheckstyleResult analyzeCheckstyle() {
161161
final StyleEventCollector collector = new StyleEventCollector()
162162
.apply(CheckstyleConfigs.minimalCheckstyleIgnore())
163-
.just(In.locs("Color", "Arrow", "Rank", "Shape", "Token", "Style", "Options", "Records", "SystemUtils", "GraphAttr").ignore("empty.line.separator"))
163+
.just(In.locs("Color", "Arrow", "Rank", "Shape", "Token", "Style", "Options", "Records", "SystemUtils", "GraphAttr", "SvgElementFinder").ignore("empty.line.separator"))
164164
.just(In.locs("EngineResult", "IOFunction").ignore("abbreviation.as.word"))
165165
.just(In.clazz(Renderer.class).ignore("indentation.error"));
166166
final StyleChecks checks = CheckstyleConfigs.adjustedGoogleStyleChecks();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright © 2015 Stefan Niederhauser ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package guru.nidi.graphviz;
17+
18+
import guru.nidi.graphviz.engine.*;
19+
import guru.nidi.graphviz.model.*;
20+
21+
import javax.swing.*;
22+
import java.awt.*;
23+
import java.awt.event.MouseEvent;
24+
import java.awt.image.BufferedImage;
25+
import java.io.IOException;
26+
import java.util.*;
27+
import java.util.List;
28+
29+
import static guru.nidi.graphviz.engine.Format.PNG;
30+
import static java.util.stream.Collectors.toList;
31+
32+
class GraphvizPanel extends JPanel {
33+
private final Dimension size;
34+
private final BufferedImage img;
35+
private double scaleX = 1, scaleY = 1;
36+
private List<Map.Entry<Rectangle, String>> boxes;
37+
38+
GraphvizPanel(Graph g, Dimension size) {
39+
this.size = size;
40+
img = Graphviz.fromGraph(g).postProcessor((EngineResult source, Options options, ProcessOptions processOptions) -> {
41+
try {
42+
final String svg = source.asString();
43+
final GraphElementFinder finder = new SvgElementFinder(svg).fromGraph(g);
44+
final SvgShapeAnalyzer analyzer = new SvgShapeAnalyzer(finder, size.width, size.height);
45+
boxes = finder.findNodes().stream()
46+
.map(node -> {
47+
final String tooltip = (String) finder.nodeOf(node).attrs().get("tooltip");
48+
return tooltip == null ? null : new AbstractMap.SimpleEntry<>(analyzer.getBoundingBox(node), tooltip);
49+
})
50+
.filter(Objects::nonNull)
51+
.collect(toList());
52+
return EngineResult.fromString(svg);
53+
} catch (IOException e) {
54+
throw new RuntimeException(e);
55+
}
56+
}).width(size.width).height(size.height).render(PNG).toImage();
57+
ToolTipManager.sharedInstance().registerComponent(this);
58+
}
59+
60+
@Override
61+
public Dimension getPreferredSize() {
62+
return size;
63+
}
64+
65+
@Override
66+
public String getToolTipText(MouseEvent e) {
67+
for (final Map.Entry<Rectangle, String> box : boxes) {
68+
if (box.getKey().contains(e.getX() / scaleX, e.getY() / scaleY)) {
69+
return box.getValue();
70+
}
71+
}
72+
return null;
73+
}
74+
75+
@Override
76+
protected void paintComponent(Graphics g) {
77+
super.paintComponent(g);
78+
final Rectangle c = g.getClipBounds();
79+
scaleX = c.getWidth() / size.width;
80+
scaleY = c.getHeight() / size.height;
81+
g.drawImage(img, 0, 0, c.width, c.height, null);
82+
// for (Map.Entry<Rectangle, String> box : boxes) {
83+
// g.drawRect(box.getKey().x, box.getKey().y, box.getKey().width, box.getKey().height);
84+
// }
85+
}
86+
}

0 commit comments

Comments
 (0)