SmetanaSolverB.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2025, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 *
 * If you like this project or if you find it useful, you can support us at:
 *
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 *
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 *
 */
package net.sourceforge.plantuml.posimo;

import static gen.lib.cgraph.attr__c.agsafeset;
import static gen.lib.cgraph.edge__c.agedge;
import static gen.lib.cgraph.graph__c.agopen;
import static gen.lib.cgraph.node__c.agnode;
import static gen.lib.gvc.gvc__c.gvContext;
import static gen.lib.gvc.gvlayout__c.gvLayoutJobs;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import h.ST_Agedge_s;
import h.ST_Agedgeinfo_t;
import h.ST_Agnode_s;
import h.ST_Agnodeinfo_t;
import h.ST_Agraph_s;
import h.ST_Agraphinfo_t;
import h.ST_GVC_s;
import h.ST_bezier;
import h.ST_boxf;
import h.ST_pointf;
import h.ST_splines;
import net.sourceforge.plantuml.klimt.geom.XDimension2D;
import net.sourceforge.plantuml.klimt.geom.XPoint2D;
import net.sourceforge.plantuml.klimt.shape.DotPath;
import net.sourceforge.plantuml.teavm.TeaVM;
import smetana.core.CString;
import smetana.core.Globals;

/**
 * Same job as {@link GraphvizSolverB}, but with Smetana (the Java port of
 * Graphviz) instead of the external {@code dot} program: it places the blocks
 * and computes the paths between them. It is what the builds without any
 * process (TeaVM) have to use.
 * <p>
 * Only the blocks directly inside the root cluster are supported, which is all
 * the text (TXT) output of the class diagrams needs.
 */
public class SmetanaSolverB {

	// The smetana port keeps its state in Globals, so layout runs are serialized
	// process-wide
	private static final Object lock = new Object();

	public XDimension2D solve(Cluster root, Collection<Path> paths) {
		if (root.getSubClusters().size() > 0)
			throw new UnsupportedOperationException("Clusters are not supported by SmetanaSolverB");

		// No monitor in the C build: it is single-threaded, and its monitors do not
		// survive a garbage collection happening while they are held
		if (TeaVM.isC())
			return solveOpen(root, paths);

		synchronized (lock) {
			return solveOpen(root, paths);
		}
	}

	private XDimension2D solveOpen(Cluster root, Collection<Path> paths) {
		final Globals zz = Globals.open();
		try {
			return solveInternal(zz, root, paths);
		} finally {
			Globals.close();
		}
	}

	private XDimension2D solveInternal(Globals zz, Cluster root, Collection<Path> paths) {
		final ST_Agraph_s g = agopen(zz, new CString("g"), zz.Agdirected, null);
		// The same separations as in GraphvizSolverB, in inches
		agsafeset(zz, g, new CString("nodesep"), new CString("0.2"), new CString(""));
		agsafeset(zz, g, new CString("ranksep"), new CString("0.2"), new CString(""));

		final Map<Block, ST_Agnode_s> nodes = new LinkedHashMap<>();
		for (Block b : root.getContents()) {
			final ST_Agnode_s node = agnode(zz, g, new CString("b" + b.getUid()), true);
			agsafeset(zz, node, new CString("shape"), new CString("box"), new CString(""));
			// Without an explicit empty label, the node is enlarged to fit its name
			agsafeset(zz, node, new CString("label"), new CString(""), new CString(""));
			final XDimension2D size = b.getSize();
			agsafeset(zz, node, new CString("width"), new CString("" + size.getWidth() / 72.0), new CString(""));
			agsafeset(zz, node, new CString("height"), new CString("" + size.getHeight() / 72.0), new CString(""));
			nodes.put(b, node);
		}

		final Map<Path, ST_Agedge_s> edges = new LinkedHashMap<>();
		for (Path p : paths) {
			final ST_Agnode_s node1 = nodes.get(p.getStart());
			final ST_Agnode_s node2 = nodes.get(p.getEnd());
			if (node1 == null || node2 == null)
				continue;

			final ST_Agedge_s e = agedge(zz, g, node1, node2, null, true);
			agsafeset(zz, e, new CString("arrowtail"), new CString("none"), new CString(""));
			agsafeset(zz, e, new CString("arrowhead"), new CString("none"), new CString(""));
			agsafeset(zz, e, new CString("minlen"), new CString("" + (p.getLength() - 1)), new CString(""));
			edges.put(p, e);
		}

		final ST_GVC_s gvc = gvContext(zz);
		gvLayoutJobs(zz, gvc, g);

		// Graphviz has the y axis going up, and the result is expected with the origin
		// at the top left corner, like the SVG output read by GraphvizSolverB
		final ST_boxf bb = (ST_boxf) ((ST_Agraphinfo_t) g.data).bb;
		final double minX = bb.LL.x;
		final double maxY = bb.UR.y;

		for (Map.Entry<Block, ST_Agnode_s> entry : nodes.entrySet()) {
			final ST_Agnodeinfo_t data = (ST_Agnodeinfo_t) entry.getValue().data;
			final double width = data.width * 72;
			final double height = data.height * 72;
			entry.getKey().setX(data.coord.x - width / 2 - minX);
			entry.getKey().setY(maxY - (data.coord.y + height / 2));
		}

		for (Map.Entry<Path, ST_Agedge_s> entry : edges.entrySet())
			entry.getKey().setDotPath(toDotPath((ST_Agedgeinfo_t) entry.getValue().data, minX, maxY));

		return new XDimension2D(bb.UR.x - bb.LL.x, bb.UR.y - bb.LL.y);
	}

	private static DotPath toDotPath(ST_Agedgeinfo_t data, double minX, double maxY) {
		DotPath result = new DotPath();
		final ST_splines splines = data.spl;
		if (splines == null)
			return result;

		for (int i = 0; i < splines.size; i++) {
			final ST_bezier bezier = splines.list.get__(i);
			for (int j = 0; j + 3 < bezier.size; j += 3)
				result = result.addCurve(getPoint(bezier, j, minX, maxY), getPoint(bezier, j + 1, minX, maxY),
						getPoint(bezier, j + 2, minX, maxY), getPoint(bezier, j + 3, minX, maxY));

		}
		return result;
	}

	private static XPoint2D getPoint(ST_bezier bezier, int i, double minX, double maxY) {
		final ST_pointf pt = bezier.list.get__(i);
		return new XPoint2D(pt.x - minX, maxY - pt.y);
	}

}