dot has colors now
This commit is contained in:
@@ -10,18 +10,17 @@ public class Dot implements StateMachineExporter {
|
||||
|
||||
@Override
|
||||
public String export(List<Transition> transitions,
|
||||
Set<String> startStates,
|
||||
Set<String> endStates,
|
||||
boolean useLambdaGuards) {
|
||||
Set<String> startStates,
|
||||
Set<String> endStates,
|
||||
boolean useLambdaGuards) {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("digraph stateMachine {\n");
|
||||
sb.append(" rankdir=LR;\n");
|
||||
sb.append(" node [shape=circle, style=filled, fillcolor=lightgray];\n\n");
|
||||
|
||||
// 1. Fake start node
|
||||
sb.append(" start [shape=point, label=\"\"];\n");
|
||||
|
||||
// 2. Find states with choice transitions
|
||||
Set<String> statesWithChoice = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.getType())) {
|
||||
@@ -31,18 +30,20 @@ public class Dot implements StateMachineExporter {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Declare normal states and choice pseudostates
|
||||
Set<String> allStates = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if (t.getSourceStates() != null) {
|
||||
for (String s : t.getSourceStates()) allStates.add(simplify(s));
|
||||
for (String s : t.getSourceStates()) {
|
||||
allStates.add(simplify(s));
|
||||
}
|
||||
}
|
||||
if (t.getTargetStates() != null) {
|
||||
for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt));
|
||||
for (String tgt : t.getTargetStates()) {
|
||||
allStates.add(simplify(tgt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print all normal states (except choice pseudostates for now)
|
||||
for (String state : allStates) {
|
||||
if (!statesWithChoice.contains(state + "_choice")) {
|
||||
String shape = endStates.contains(state) ? "doublecircle" : "circle";
|
||||
@@ -50,46 +51,40 @@ public class Dot implements StateMachineExporter {
|
||||
.append(" [shape=").append(shape).append(", fillcolor=lightgray];\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// Declare choice pseudostates as diamonds
|
||||
for (String state : statesWithChoice) {
|
||||
String choiceNode = state + "_choice";
|
||||
sb.append(" ").append(choiceNode)
|
||||
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
// 4. Connect start node(s) to start states
|
||||
for (String start : startStates) {
|
||||
sb.append(" start -> ").append(simplify(start)).append(";\n");
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
// 5. Connect normal states to their choice pseudostates
|
||||
for (String state : statesWithChoice) {
|
||||
String choiceNode = state + "_choice";
|
||||
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
||||
for (Transition t : transitions) {
|
||||
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||
|
||||
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||
List<String> targets = t.getTargetStates();
|
||||
|
||||
List<String> targets;
|
||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||
if (targets == null || targets.isEmpty()) {
|
||||
if (!allowNoTarget) {
|
||||
// self-loop: targets = sources
|
||||
targets = t.getSourceStates();
|
||||
targets = t.getSourceStates(); // self-loop
|
||||
} else {
|
||||
// withJoin/withFork with no targets: skip
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
targets = t.getTargetStates();
|
||||
}
|
||||
|
||||
for (String rawSource : t.getSourceStates()) {
|
||||
@@ -103,54 +98,97 @@ public class Dot implements StateMachineExporter {
|
||||
edgeSource = source + "_choice";
|
||||
}
|
||||
|
||||
// Prepare guard text
|
||||
// Guard
|
||||
String guardText = "";
|
||||
if (t.getGuard() != null && !t.getGuard().isBlank()) {
|
||||
if (useLambdaGuards && t.isLambdaGuard()) {
|
||||
guardText = "lambda";
|
||||
guardText = "λ";
|
||||
} else {
|
||||
guardText = t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
guardText = escapeDotString(
|
||||
t.getGuard().replaceAll("[\\n\\r]+", " ")
|
||||
.replaceAll("\\s{2,}", " ")
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare actions text
|
||||
// Actions
|
||||
String actionsText = "";
|
||||
if (t.getActions() != null && !t.getActions().isEmpty()) {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.getActions().size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.getActions().get(i);
|
||||
boolean isLambda = t.getIsLambdaActions().get(i);
|
||||
boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i);
|
||||
if (useLambdaGuards && isLambda) {
|
||||
actionBuilder.append("lambda");
|
||||
actionBuilder.append("λ");
|
||||
} else {
|
||||
actionBuilder.append(action);
|
||||
actionBuilder.append(escapeDotString(action));
|
||||
}
|
||||
}
|
||||
actionsText = actionBuilder.toString();
|
||||
}
|
||||
|
||||
// Build label with event, guard, and actions
|
||||
// Event and label
|
||||
StringBuilder label = new StringBuilder();
|
||||
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
||||
label.append(simplify(t.getEvent()));
|
||||
label.append(escapeDotString(simplify(t.getEvent())));
|
||||
}
|
||||
|
||||
if (!guardText.isBlank()) {
|
||||
if (!label.isEmpty()) label.append(" ");
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
|
||||
if (!actionsText.isBlank()) {
|
||||
if (!label.isEmpty()) label.append(" / ");
|
||||
label.append(actionsText);
|
||||
}
|
||||
|
||||
// Add order info for choice/junction transitions
|
||||
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
|
||||
if (!label.isEmpty()) label.append(" ");
|
||||
label.append("(order=").append(t.getOrder()).append(")");
|
||||
}
|
||||
|
||||
String edgeLabel = !label.isEmpty() ? " [label=\"" + label.toString() + "\"]" : "";
|
||||
// Build edge attributes
|
||||
StringBuilder edgeAttributes = new StringBuilder();
|
||||
if (!label.isEmpty()) {
|
||||
appendEdgeAttr(edgeAttributes, "label", label.toString());
|
||||
}
|
||||
|
||||
switch (t.getType()) {
|
||||
case "withInternal":
|
||||
appendEdgeAttr(edgeAttributes, "style", "dashed");
|
||||
appendEdgeAttr(edgeAttributes, "color", "gray");
|
||||
break;
|
||||
case "withLocal":
|
||||
appendEdgeAttr(edgeAttributes, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttributes, "color", "darkgray");
|
||||
break;
|
||||
case "withChoice":
|
||||
appendEdgeAttr(edgeAttributes, "style", "solid");
|
||||
appendEdgeAttr(edgeAttributes, "color", "blue");
|
||||
break;
|
||||
case "withJunction":
|
||||
appendEdgeAttr(edgeAttributes, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttributes, "color", "purple");
|
||||
break;
|
||||
case "withFork":
|
||||
appendEdgeAttr(edgeAttributes, "style", "bold");
|
||||
appendEdgeAttr(edgeAttributes, "color", "green");
|
||||
break;
|
||||
case "withJoin":
|
||||
appendEdgeAttr(edgeAttributes, "style", "bold");
|
||||
appendEdgeAttr(edgeAttributes, "color", "darkorange");
|
||||
break;
|
||||
case "withExternal":
|
||||
default:
|
||||
appendEdgeAttr(edgeAttributes, "style", "solid");
|
||||
appendEdgeAttr(edgeAttributes, "color", "black");
|
||||
break;
|
||||
}
|
||||
|
||||
String edgeLabel = !edgeAttributes.isEmpty() ? " [" + edgeAttributes + "]" : "";
|
||||
|
||||
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
|
||||
}
|
||||
@@ -165,4 +203,15 @@ public class Dot implements StateMachineExporter {
|
||||
public String getFileExtension() {
|
||||
return ".dot";
|
||||
}
|
||||
|
||||
private String escapeDotString(String input) {
|
||||
if (input == null) return "";
|
||||
return input.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private void appendEdgeAttr(StringBuilder sb, String key, String value) {
|
||||
if (!sb.isEmpty()) sb.append(", ");
|
||||
sb.append(key).append("=\"").append(value).append("\"");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user