aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java')
-rw-r--r--subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java335
1 files changed, 335 insertions, 0 deletions
diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java
new file mode 100644
index 00000000..a6a3dc69
--- /dev/null
+++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java
@@ -0,0 +1,335 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.visualization.internal;
7
8import tools.refinery.store.map.Version;
9import tools.refinery.store.model.Interpretation;
10import tools.refinery.store.model.Model;
11import tools.refinery.store.representation.AnySymbol;
12import tools.refinery.store.representation.TruthValue;
13import tools.refinery.store.tuple.Tuple;
14import tools.refinery.visualization.ModelVisualizerAdapter;
15import tools.refinery.visualization.ModelVisualizerStoreAdapter;
16import tools.refinery.visualization.statespace.VisualizationStore;
17
18import java.io.*;
19import java.util.*;
20import java.util.stream.Collectors;
21
22public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter {
23 private final Model model;
24 private final ModelVisualizerStoreAdapter storeAdapter;
25 private final Map<AnySymbol, Interpretation<?>> allInterpretations;
26 private final StringBuilder designSpaceBuilder = new StringBuilder();
27 private final Map<Version, Integer> states = new HashMap<>();
28 private int transitionCounter = 0;
29 private Integer numberOfStates = 0;
30 private final String outputPath;
31 private final Set<FileFormat> formats;
32 private final boolean renderDesignSpace;
33 private final boolean renderStates;
34
35 private static final Map<Object, String> truthValueToDot = Map.of(
36 TruthValue.TRUE, "1",
37 TruthValue.FALSE, "0",
38 TruthValue.UNKNOWN, "½",
39 TruthValue.ERROR, "E",
40 true, "1",
41 false, "0"
42 );
43
44 public ModelVisualizerAdapterImpl(Model model, ModelVisualizerStoreAdapter storeAdapter) {
45 this.model = model;
46 this.storeAdapter = storeAdapter;
47 this.outputPath = storeAdapter.getOutputPath();
48 this.formats = storeAdapter.getFormats();
49 if (formats.isEmpty()) {
50 formats.add(FileFormat.SVG);
51 }
52 this.renderDesignSpace = storeAdapter.isRenderDesignSpace();
53 this.renderStates = storeAdapter.isRenderStates();
54
55 this.allInterpretations = new HashMap<>();
56 for (var symbol : storeAdapter.getStore().getSymbols()) {
57 var arity = symbol.arity();
58 if (arity < 1 || arity > 2) {
59 continue;
60 }
61 var interpretation = (Interpretation<?>) model.getInterpretation(symbol);
62 allInterpretations.put(symbol, interpretation);
63 }
64 designSpaceBuilder.append("digraph designSpace {\n");
65 designSpaceBuilder.append("""
66 nodesep=0
67 ranksep=5
68 node[
69 \tstyle=filled
70 \tfillcolor=white
71 ]
72 """);
73 }
74
75 @Override
76 public Model getModel() {
77 return model;
78 }
79
80 @Override
81 public ModelVisualizerStoreAdapter getStoreAdapter() {
82 return storeAdapter;
83 }
84
85 private String createDotForCurrentModelState() {
86
87 var unaryTupleToInterpretationsMap = new HashMap<Tuple, LinkedHashSet<Interpretation<?>>>();
88
89 var sb = new StringBuilder();
90
91 sb.append("digraph model {\n");
92 sb.append("""
93 node [
94 \tstyle="filled, rounded"
95 \tshape=plain
96 \tpencolor="#00000088"
97 \tfontname="Helvetica"
98 ]
99 """);
100 sb.append("""
101 edge [
102 \tlabeldistance=3
103 \tfontname="Helvetica"
104 ]
105 """);
106
107 for (var entry : allInterpretations.entrySet()) {
108 var key = entry.getKey();
109 var arity = key.arity();
110 var cursor = entry.getValue().getAll();
111 if (arity == 1) {
112 while (cursor.move()) {
113 unaryTupleToInterpretationsMap.computeIfAbsent(cursor.getKey(), k -> new LinkedHashSet<>())
114 .add(entry.getValue());
115 }
116 } else if (arity == 2) {
117 while (cursor.move()) {
118 var tuple = cursor.getKey();
119 for (var i = 0; i < tuple.getSize(); i++) {
120 var id = tuple.get(i);
121 unaryTupleToInterpretationsMap.computeIfAbsent(Tuple.of(id), k -> new LinkedHashSet<>());
122 }
123 sb.append(drawEdge(cursor.getKey(), key, entry.getValue()));
124 }
125 }
126 }
127 for (var entry : unaryTupleToInterpretationsMap.entrySet()) {
128 sb.append(drawElement(entry));
129 }
130 sb.append("}");
131 return sb.toString();
132 }
133
134 private StringBuilder drawElement(Map.Entry<Tuple, LinkedHashSet<Interpretation<?>>> entry) {
135 var sb = new StringBuilder();
136
137 var tableStyle = " CELLSPACING=\"0\" BORDER=\"2\" CELLBORDER=\"0\" CELLPADDING=\"4\" STYLE=\"ROUNDED\"";
138
139 var key = entry.getKey();
140 var id = key.get(0);
141 var mainLabel = String.valueOf(id);
142 var interpretations = entry.getValue();
143 var backgroundColor = toBackgroundColorString(averageColor(interpretations));
144
145 sb.append(id);
146 sb.append(" [\n");
147 sb.append("\tfillcolor=\"").append(backgroundColor).append("\"\n");
148 sb.append("\tlabel=");
149 if (interpretations.isEmpty()) {
150 sb.append("<<TABLE").append(tableStyle).append(">\n\t<TR><TD>").append(mainLabel).append("</TD></TR>");
151 }
152 else {
153 sb.append("<<TABLE").append(tableStyle).append(">\n\t\t<TR><TD COLSPAN=\"3\" BORDER=\"2\" SIDES=\"B\">")
154 .append(mainLabel).append("</TD></TR>\n");
155 for (var interpretation : interpretations) {
156 var rawValue = interpretation.get(key);
157
158 if (rawValue == null || rawValue.equals(TruthValue.FALSE) || rawValue.equals(false)) {
159 continue;
160 }
161 var color = "black";
162 if (rawValue.equals(TruthValue.ERROR)) {
163 color = "red";
164 }
165 var value = truthValueToDot.getOrDefault(rawValue, rawValue.toString());
166 var symbol = interpretation.getSymbol();
167
168 if (symbol.valueType() == String.class) {
169 value = "\"" + value + "\"";
170 }
171 sb.append("\t\t<TR><TD><FONT COLOR=\"").append(color).append("\">")
172 .append(interpretation.getSymbol().name())
173 .append("</FONT></TD><TD><FONT COLOR=\"").append(color).append("\">")
174 .append("=</FONT></TD><TD><FONT COLOR=\"").append(color).append("\">").append(value)
175 .append("</FONT></TD></TR>\n");
176 }
177 }
178 sb.append("\t\t</TABLE>>\n");
179 sb.append("]\n");
180
181 return sb;
182 }
183
184 private String drawEdge(Tuple edge, AnySymbol symbol, Interpretation<?> interpretation) {
185 var value = interpretation.get(edge);
186
187 if (value == null || value.equals(TruthValue.FALSE) || value.equals(false)) {
188 return "";
189 }
190
191 var sb = new StringBuilder();
192 var style = "solid";
193 var color = "black";
194 if (value.equals(TruthValue.UNKNOWN)) {
195 style = "dotted";
196 }
197 else if (value.equals(TruthValue.ERROR)) {
198 style = "dashed";
199 color = "red";
200 }
201
202 var from = edge.get(0);
203 var to = edge.get(1);
204 var name = symbol.name();
205 sb.append(from).append(" -> ").append(to)
206 .append(" [\n\tstyle=").append(style)
207 .append("\n\tcolor=").append(color)
208 .append("\n\tfontcolor=").append(color)
209 .append("\n\tlabel=\"").append(name)
210 .append("\"]\n");
211 return sb.toString();
212 }
213
214 private String toBackgroundColorString(Integer[] backgroundColor) {
215 if (backgroundColor.length == 3)
216 return String.format("#%02x%02x%02x", backgroundColor[0], backgroundColor[1], backgroundColor[2]);
217 else if (backgroundColor.length == 4)
218 return String.format("#%02x%02x%02x%02x", backgroundColor[0], backgroundColor[1], backgroundColor[2],
219 backgroundColor[3]);
220 return null;
221 }
222
223 private Integer[] typeColor(String name) {
224 @SuppressWarnings("squid:S2245")
225 var random = new Random(name.hashCode());
226 return new Integer[] { random.nextInt(128) + 128, random.nextInt(128) + 128, random.nextInt(128) + 128 };
227 }
228
229 private Integer[] averageColor(Set<Interpretation<?>> interpretations) {
230 if(interpretations.isEmpty()) {
231 return new Integer[]{256, 256, 256};
232 }
233 // TODO: Only use interpretations where the value is not false (or unknown)
234 var symbols = interpretations.stream()
235 .map(i -> typeColor(i.getSymbol().name())).toArray(Integer[][]::new);
236
237
238
239 return new Integer[] {
240 Arrays.stream(symbols).map(i -> i[0]).collect(Collectors.averagingInt(Integer::intValue)).intValue(),
241 Arrays.stream(symbols).map(i -> i[1]).collect(Collectors.averagingInt(Integer::intValue)).intValue(),
242 Arrays.stream(symbols).map(i -> i[2]).collect(Collectors.averagingInt(Integer::intValue)).intValue()
243 };
244 }
245
246 private String createDotForModelState(Version version) {
247 var currentVersion = model.getState();
248 model.restore(version);
249 var graph = createDotForCurrentModelState();
250 model.restore(currentVersion);
251 return graph;
252 }
253
254 private boolean saveDot(String dot, String filePath) {
255 File file = new File(filePath);
256 file.getParentFile().mkdirs();
257
258 try (FileWriter writer = new FileWriter(file)) {
259 writer.write(dot);
260 } catch (Exception e) {
261 e.printStackTrace();
262 return false;
263 }
264 return true;
265 }
266
267 private boolean renderDot(String dot, String filePath) {
268 return renderDot(dot, FileFormat.SVG, filePath);
269 }
270
271 private boolean renderDot(String dot, FileFormat format, String filePath) {
272 try {
273 Process process = new ProcessBuilder("dot", "-T" + format.getFormat(), "-o", filePath).start();
274
275 OutputStream osToProcess = process.getOutputStream();
276 PrintWriter pwToProcess = new PrintWriter(osToProcess);
277 pwToProcess.write(dot);
278 pwToProcess.close();
279 } catch (IOException e) {
280 e.printStackTrace();
281 return false;
282 }
283 return true;
284 }
285
286 private String buildDesignSpaceDot() {
287 designSpaceBuilder.append("}");
288 return designSpaceBuilder.toString();
289 }
290
291 private boolean saveDesignSpace(String path) {
292 saveDot(buildDesignSpaceDot(), path + "/designSpace.dot");
293 for (var entry : states.entrySet()) {
294 saveDot(createDotForModelState(entry.getKey()), path + "/" + entry.getValue() + ".dot");
295 }
296 return true;
297 }
298
299 private void renderDesignSpace(String path, Set<FileFormat> formats) {
300 File filePath = new File(path);
301 filePath.mkdirs();
302 if (renderStates) {
303 for (var entry : states.entrySet()) {
304 var stateId = entry.getValue();
305 var stateDot = createDotForModelState(entry.getKey());
306 for (var format : formats) {
307 if (format == FileFormat.DOT) {
308 saveDot(stateDot, path + "/" + stateId + ".dot");
309 }
310 else {
311 renderDot(stateDot, format, path + "/" + stateId + "." + format.getFormat());
312 }
313 }
314 }
315 }
316 if (renderDesignSpace) {
317 var designSpaceDot = buildDesignSpaceDot();
318 for (var format : formats) {
319 if (format == FileFormat.DOT) {
320 saveDot(designSpaceDot, path + "/designSpace.dot");
321 }
322 else {
323 renderDot(designSpaceDot, format, path + "/designSpace." + format.getFormat());
324 }
325 }
326 }
327 }
328
329 @Override
330 public void visualize(VisualizationStore visualizationStore) {
331 this.designSpaceBuilder.append(visualizationStore.getDesignSpaceStringBuilder());
332 this.states.putAll(visualizationStore.getStates());
333 renderDesignSpace(outputPath, formats);
334 }
335}