Skip to content
Merged
123 changes: 120 additions & 3 deletions components/camel-diagram/src/main/docs/diagram.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
*Since Camel {since}*

The Diagram module provides route diagram rendering capabilities for Apache Camel routes.
It can generate visual route diagrams as PNG images representations from route structure data.
It can generate visual route diagrams as PNG images or plain ASCII art text representations from route structure data.

== Features

* Render route diagrams as PNG images with colored nodes and scope boxes
* Render route diagrams as plain ASCII art text for terminal output
* Support for all Camel EIPs: choice, doTry/doCatch, filter, split, loop, multicast, and more
* Scope boxes visually group branching and scoping EIPs
* Multiple color themes: dark, light, transparent, or custom
* Multiple color themes: dark, light, transparent, or custom (PNG only)

== Usage

Expand All @@ -35,14 +36,22 @@ Add the `camel-diagram` dependency to your project:

==== Using Camel Java API

You can use the diagram render with Camel based APIs such as:
You can use the diagram renderer with the Camel API to render as PNG images:

[source,java]
----
RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(context);
BufferedImage image = dumper.dumpRoutesAsImage("*", RouteDiagramDumper.Theme.DARK);
----

Or render as ASCII art text:

[source,java]
----
RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(context);
String ascii = dumper.dumpRoutesAsAsciiArt("*");
----

==== Using standalone Java API

Then use the API to render diagrams:
Expand All @@ -68,6 +77,25 @@ BufferedImage image = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagram
ImageIO.write(image, "PNG", new File("diagram.png"));
----

To render as ASCII art instead:

[source,java]
----
import org.apache.camel.diagram.*;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.*;

// Parse route structure from JSON
List<RouteInfo> routes = RouteDiagramHelper.parseRoutes(jsonObject);

// Layout and render as ASCII
RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
LayoutRoute lr = engine.layoutRoute(routes.get(0), RouteDiagramLayoutEngine.PADDING);

RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
String ascii = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
System.out.println(ascii);
----

=== With Camel JBang

The diagram rendering is used by the `camel cmd route-diagram` command in Camel JBang:
Expand Down Expand Up @@ -100,3 +128,92 @@ To use dark theme
----
camel cmd route-diagram MyRoute.java --theme=dark
----

== ASCII Art Rendering

The ASCII art renderer produces plain text diagrams using box-drawing characters.
This is useful for terminal output where images cannot be displayed.

Nodes are drawn as boxes using `+`, `-`, and `|` characters, with arrows using `|` and `v`.
Branching EIPs (choice, multicast, etc.) produce L-shaped arrows with horizontal connector lines.
Long labels are automatically wrapped to fit within the box width.

Example output for a simple route:

[source,text]
----
route1
+----------------------+
| timer:tick |
+----------------------+
|
|
|
v
+----------------------+
| log:a |
+----------------------+
----

Example output for a branching route with choice:

[source,text]
----
route1
+----------------------+
| timer:tick |
+----------------------+
|
v
+----------------------+
| choice() |
+----------------------+
|
+---------------+---------------+
v v
+----------------------+ +----------------------+
| when(...) | | otherwise() |
+----------------------+ +----------------------+
| |
v v
+----------------------+ +----------------------+
| log:a | | log:b |
+----------------------+ +----------------------+
----

Scope boxes (for filter, split, doTry, etc.) are rendered with dashed borders using `:` for vertical
and `- - -` for horizontal lines.

Use `--theme=ascii` for plain ASCII art:

[source,bash]
----
camel cmd route-diagram MyRoute.java --theme=ascii
----

== Unicode Rendering

The `unicode` theme uses Unicode box-drawing characters for a cleaner look.
Node boxes use `┌──┐ │ └──┘`, arrows use `│` and `▼`, and branch junctions use `┴`.
Scope boxes use `╌` (dashed horizontal) and `╎` (dashed vertical) with no corners.

[source,bash]
----
camel cmd route-diagram MyRoute.java --theme=unicode
----

Example output:

[source,text]
----
route1
┌──────────────────────┐
│ timer:tick │
└──────────────────────┘
┌──────────────────────┐
│ log:a │
└──────────────────────┘
----
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ public BufferedImage dumpRoutesAsImage(
return renderImage(routes, theme.name(), fontSize, nodeWidth, nodeLabel.name(), metrics);
}

@Override
public String dumpRoutesAsAsciiArt(
String filter, RouteDiagramDumper.NodeLabelMode nodeLabel, int nodeWidth, boolean unicode) {
DevConsole dc = getCamelContext().getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("route-structure");
JsonObject root = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("filter", filter));
var routes = RouteDiagramHelper.parseRoutes(root);
return renderAscii(routes, nodeWidth, nodeLabel.name(), unicode);
}

@Override
public String imageToBase64(BufferedImage image) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Expand Down Expand Up @@ -152,4 +162,23 @@ private static BufferedImage renderImage(
return renderer.renderDiagram(layoutRoutes, currentY, colors);
}

private static String renderAscii(
List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel, boolean unicode) {
RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));

List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new ArrayList<>();
int currentY = RouteDiagramLayoutEngine.PADDING;
for (RouteDiagramLayoutEngine.RouteInfo route : routes) {
RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(route, currentY);
layoutRoutes.add(lr);
currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
}

RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode);
return renderer.renderDiagram(layoutRoutes, currentY);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class DiagramDevConsole extends AbstractDevConsole {
public static final String FILTER = "filter";

/**
* Theme to use: dark or light
* Theme to use: dark, light, transparent, ascii, or unicode
*/
public static final String THEME = "theme";

Expand Down Expand Up @@ -84,18 +84,25 @@ protected String doCallText(Map<String, Object> options) {

try {
RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(getCamelContext());
BufferedImage image = dumper.dumpRoutesAsImage(filter, RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
String base64 = dumper.imageToBase64(image);
// For HTML embedding:
String html = String.format(
" <body>\n <img src=\"data:image/png;base64,%s\" alt=\"Route Diagram\">\n </body>\n",
base64);
if (refresh) {
html = "<head><meta http-equiv=\"refresh\" content=\"5\"></head>\n" + html;
if (isTextTheme(theme)) {
String text = dumper.dumpRoutesAsAsciiArt(filter,
RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()),
nodeWidth, isUnicodeTheme(theme));
sj.add(text);
} else {
BufferedImage image = dumper.dumpRoutesAsImage(filter,
RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
String base64 = dumper.imageToBase64(image);
String html = String.format(
" <body>\n <img src=\"data:image/png;base64,%s\" alt=\"Route Diagram\">\n </body>\n",
base64);
if (refresh) {
html = "<head><meta http-equiv=\"refresh\" content=\"5\"></head>\n" + html;
}
html = "<html>\n" + html + "</html>\n";
sj.add(html);
}
html = "<html>\n" + html + "</html>\n";
sj.add(html);
} catch (Exception e) {
// ignore
}
Expand All @@ -117,15 +124,31 @@ protected Map<String, Object> doCallJson(Map<String, Object> options) {
JsonObject root = new JsonObject();
try {
RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(getCamelContext());
BufferedImage image = dumper.dumpRoutesAsImage(filter, RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
String base64 = dumper.imageToBase64(image);
root.put("image", base64);
if (isTextTheme(theme)) {
String text = dumper.dumpRoutesAsAsciiArt(filter,
RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()),
nodeWidth, isUnicodeTheme(theme));
root.put("text", text);
} else {
BufferedImage image = dumper.dumpRoutesAsImage(filter,
RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
String base64 = dumper.imageToBase64(image);
root.put("image", base64);
}
} catch (Exception e) {
// ignore
}

return root;
}

private static boolean isTextTheme(String theme) {
return "ascii".equalsIgnoreCase(theme) || "unicode".equalsIgnoreCase(theme);
}

private static boolean isUnicodeTheme(String theme) {
return "unicode".equalsIgnoreCase(theme);
}

}
Loading