DotExporter.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.analysis.modes.reachability;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.osate.analysis.modes.modemodel.ActiveNode;
import org.osate.analysis.modes.modemodel.FeatureTrigger;
import org.osate.analysis.modes.modemodel.InactiveNode;
import org.osate.analysis.modes.modemodel.SOMGraph;
import org.osate.analysis.modes.modemodel.SOMNode;

public class DotExporter extends FileExporter {

	private static final String FILE_EXT = "dot";

	private SOMGraph graph;

	private static final boolean SKIP_DEAD_NODES = true;

	private Set<SOMNode> deadNodes = new HashSet<>();

	DotExporter(SOMGraph graph) {
		this.graph = graph;
	}

	@Override
	SOMGraph getGraph() {
		return graph;
	}

	@Override
	CharSequence getContent() {
		if (SKIP_DEAD_NODES) {
			deadNodes.clear();
			collectDeadNodes();
		}
		return generateDOT();
	}

	private void collectDeadNodes() {
		var levels = getGraph().getLevels();
		int levelCount = levels.size();
		for (int i = levelCount - 2; i >= 0; i--) {
			for (var pn : levels.get(i).getNodes()) {
				boolean dead = true;
				for (var cn : pn.getChildren()) {
					if (cn.isReachable() && !deadNodes.contains(cn)) {
						dead = false;
						break;
					}
				}
				if (dead) {
					deadNodes.add(pn);
				}
			}
		}
	}

	@Override
	String getFileExtension() {
		return FILE_EXT;
	}

	StringBuilder generateDOT() {
		var node2n = new HashMap<SOMNode, Integer>();
		var b = new StringBuilder();
		b.append("digraph {\n");
		b.append("  newrank=true\n");
		b.append("  compound=true\n");
		b.append("  labeljust=l\n");
		b.append("  nodesep=0.5 ranksep=0.5\n");

		var n = 1;
		final int lastLevel = graph.getLevels().size() - 1;
		for (int l = 0; l <= lastLevel; l++) {
			var level = graph.getLevels().get(l);
			var n0 = n;
			var label = level.getComponent().getFullName();
			b.append("  subgraph l" + l + " {\n");
			b.append("    cluster=true\n");
			b.append("    label=\"" + label + "\"\n");
			for (var node : level.getNodes()) {
				if (deadNodes.contains(node)) {
					continue;
				}
				label = "none";
				if (node instanceof ActiveNode mn) {
					label = "\u22a4"; // Top
					if (mn.hasMode()) {
						label += "[" + mn.getMode().getName() + "]";
					}
				} else if (node instanceof InactiveNode in) {
					label = "\u22a5"; // Bottom
					if (in.hasMode()) {
						label += "[" + in.getMode().getName() + "]";
					}
				}
				var styles = new ArrayList<String>();
				if (node == level.getInitialNode()) {
					styles.add("filled");
				}
				if (node.isDerived()) {
					styles.add("dashed");
				}
				String style = styles.isEmpty() ? "" : " style=\"" + String.join(",", styles) + "\"";
				b.append("    " + n + " [label=\"" + label + "\"" + style + "]\n");
				node2n.put(node, n);
				n += 1;
			}
			if (l != lastLevel || level.getNodes().size() == 2) {
				b.append("    rank=same\n");
			}
			b.append("  }\n");
			n = n0;
			for (var node : level.getNodes()) {
				if (SKIP_DEAD_NODES && deadNodes.contains(node)) {
					continue;
				}
				if (l != 0) {
					b.append("  " + node2n.get(node.getParent()) + " -> " + n);
				}
				b.append(" [color=red arrowhead=none");
				if (!node.isReachable()) {
					b.append(" style=dashed");
				}
				b.append("]\n");
				n += 1;
			}
		}

		var level = graph.getLevels().get(lastLevel);
		for (var tn : level.getTransitions()) {
			var s = node2n.get(tn.getSrc());
			var d = node2n.get(tn.getDst());
			var label = "";
			if (tn.getTrigger() instanceof FeatureTrigger ftg) {
				label = ftg.getFeature().getComponentInstancePath();
			}
			b.append("  " + s + " -> " + d + " [label=\"" + label + "\" color=\"#aaaaaa\"]\n");
		}

		b.append("}\n");
		return b;
	}

}