/* ======================================================================== * PlantUML : a free UML diagram generator * ======================================================================== * * (C) Copyright 2009-2023, Arnaud Roques * * Project Info: http://plantuml.com * * If you like this project or if you find it useful, you can support us at: * * http://plantuml.com/patreon (only 1$ per month!) * http://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.elk; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.sourceforge.plantuml.AlignmentParam; import net.sourceforge.plantuml.FileFormatOption; import net.sourceforge.plantuml.FontParam; import net.sourceforge.plantuml.ISkinParam; import net.sourceforge.plantuml.StringUtils; import net.sourceforge.plantuml.UmlDiagram; import net.sourceforge.plantuml.UmlDiagramType; import net.sourceforge.plantuml.api.ImageDataSimple; import net.sourceforge.plantuml.awt.geom.XDimension2D; import net.sourceforge.plantuml.awt.geom.XPoint2D; import net.sourceforge.plantuml.core.ImageData; import net.sourceforge.plantuml.cucadiagram.CucaDiagram; import net.sourceforge.plantuml.cucadiagram.Display; import net.sourceforge.plantuml.cucadiagram.GroupType; import net.sourceforge.plantuml.cucadiagram.IEntity; import net.sourceforge.plantuml.cucadiagram.IGroup; import net.sourceforge.plantuml.cucadiagram.ILeaf; import net.sourceforge.plantuml.cucadiagram.Link; import net.sourceforge.plantuml.cucadiagram.entity.EntityFactory; /* * You can choose between real "org.eclipse.elk..." classes or proxied "net.sourceforge.plantuml.elk.proxy..." * * Using proxied classes allows to compile PlantUML without having ELK available on the classpath. * Since GraphViz is the default layout engine up to now, we do not want to enforce the use of ELK just for compilation. * (for people not using maven) * * If you are debugging, you should probably switch to "org.eclipse.elk..." classes * */ /* import org.eclipse.elk.core.RecursiveGraphLayoutEngine; import org.eclipse.elk.core.math.ElkPadding; import org.eclipse.elk.core.options.CoreOptions; import org.eclipse.elk.core.options.Direction; import org.eclipse.elk.core.options.EdgeLabelPlacement; import org.eclipse.elk.core.options.HierarchyHandling; import org.eclipse.elk.core.options.NodeLabelPlacement; import org.eclipse.elk.core.util.NullElkProgressMonitor; import org.eclipse.elk.graph.ElkEdge; import org.eclipse.elk.graph.ElkLabel; import org.eclipse.elk.graph.ElkNode; import org.eclipse.elk.graph.util.ElkGraphUtil; */ import net.sourceforge.plantuml.elk.proxy.core.RecursiveGraphLayoutEngine; import net.sourceforge.plantuml.elk.proxy.core.math.ElkPadding; import net.sourceforge.plantuml.elk.proxy.core.options.CoreOptions; import net.sourceforge.plantuml.elk.proxy.core.options.Direction; import net.sourceforge.plantuml.elk.proxy.core.options.EdgeLabelPlacement; import net.sourceforge.plantuml.elk.proxy.core.options.HierarchyHandling; import net.sourceforge.plantuml.elk.proxy.core.options.NodeLabelPlacement; import net.sourceforge.plantuml.elk.proxy.core.util.NullElkProgressMonitor; import net.sourceforge.plantuml.elk.proxy.graph.ElkEdge; import net.sourceforge.plantuml.elk.proxy.graph.ElkLabel; import net.sourceforge.plantuml.elk.proxy.graph.ElkNode; import net.sourceforge.plantuml.elk.proxy.graph.util.ElkGraphUtil; import net.sourceforge.plantuml.graphic.AbstractTextBlock; import net.sourceforge.plantuml.graphic.FontConfiguration; import net.sourceforge.plantuml.graphic.HorizontalAlignment; import net.sourceforge.plantuml.graphic.QuoteUtils; import net.sourceforge.plantuml.graphic.StringBounder; import net.sourceforge.plantuml.graphic.TextBlock; import net.sourceforge.plantuml.graphic.TextBlockUtils; import net.sourceforge.plantuml.graphic.USymbolFolder; import net.sourceforge.plantuml.log.Logme; import net.sourceforge.plantuml.style.PName; import net.sourceforge.plantuml.style.SName; import net.sourceforge.plantuml.style.Style; import net.sourceforge.plantuml.svek.Bibliotekon; import net.sourceforge.plantuml.svek.Cluster; import net.sourceforge.plantuml.svek.ClusterDecoration; import net.sourceforge.plantuml.svek.ClusterPosition; import net.sourceforge.plantuml.svek.CucaDiagramFileMaker; import net.sourceforge.plantuml.svek.DotStringFactory; import net.sourceforge.plantuml.svek.GeneralImageBuilder; import net.sourceforge.plantuml.svek.GraphvizCrash; import net.sourceforge.plantuml.svek.IEntityImage; import net.sourceforge.plantuml.svek.PackageStyle; import net.sourceforge.plantuml.svek.TextBlockBackcolored; import net.sourceforge.plantuml.ugraphic.MinMax; import net.sourceforge.plantuml.ugraphic.UGraphic; import net.sourceforge.plantuml.ugraphic.URectangle; import net.sourceforge.plantuml.ugraphic.UStroke; import net.sourceforge.plantuml.ugraphic.UTranslate; import net.sourceforge.plantuml.ugraphic.color.HColor; import net.sourceforge.plantuml.ugraphic.color.HColors; /* * Some notes: * https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure.html https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html Long hierarchical edge https://rtsys.informatik.uni-kiel.de/~biblio/downloads/theses/yab-bt.pdf https://rtsys.informatik.uni-kiel.de/~biblio/downloads/theses/thw-bt.pdf */ public class CucaDiagramFileMakerElk implements CucaDiagramFileMaker { private final CucaDiagram diagram; private final StringBounder stringBounder; private final DotStringFactory dotStringFactory; private final Map nodes = new LinkedHashMap(); private final Map clusters = new LinkedHashMap(); private final Map edges = new LinkedHashMap(); public CucaDiagramFileMakerElk(CucaDiagram diagram, StringBounder stringBounder) { this.diagram = diagram; this.stringBounder = stringBounder; this.dotStringFactory = new DotStringFactory(stringBounder, diagram); } private TextBlock getLabel(Link link) { if (Display.isNull(link.getLabel())) { return null; } final ISkinParam skinParam = diagram.getSkinParam(); final FontConfiguration labelFont = FontConfiguration.create(skinParam, FontParam.ARROW, null); final TextBlock label = link.getLabel().create(labelFont, skinParam.getDefaultTextAlignment(HorizontalAlignment.CENTER), skinParam); if (TextBlockUtils.isEmpty(label, stringBounder)) return null; return label; } private TextBlock getQuantifier(Link link, int n) { final String tmp = n == 1 ? link.getQuantifier1() : link.getQuantifier2(); if (tmp == null) return null; final ISkinParam skinParam = diagram.getSkinParam(); final FontConfiguration labelFont = FontConfiguration.create(skinParam, FontParam.ARROW, null); final TextBlock label = Display.getWithNewlines(tmp).create(labelFont, skinParam.getDefaultTextAlignment(HorizontalAlignment.CENTER), skinParam); if (TextBlockUtils.isEmpty(label, stringBounder)) return null; return label; } // Retrieve the real position of a node, depending on its parents private XPoint2D getPosition(ElkNode elkNode) { final ElkNode parent = elkNode.getParent(); final double x = elkNode.getX(); final double y = elkNode.getY(); // This nasty test checks that parent is "root" if (parent == null || parent.getLabels().size() == 0) { return new XPoint2D(x, y); } // Right now, this is recursive final XPoint2D parentPosition = getPosition(parent); return new XPoint2D(parentPosition.getX() + x, parentPosition.getY() + y); } // The Drawing class does the real drawing class Drawing extends AbstractTextBlock implements TextBlockBackcolored { // min and max of all coord private final MinMax minMax; public Drawing(MinMax minMax) { this.minMax = minMax; } public void drawU(UGraphic ug) { drawAllClusters(ug); drawAllNodes(ug); drawAllEdges(ug); } private void drawAllClusters(UGraphic ug) { for (Entry ent : clusters.entrySet()) drawSingleCluster(ug, ent.getKey(), ent.getValue()); } private void drawAllNodes(UGraphic ug) { for (Entry ent : nodes.entrySet()) drawSingleNode(ug, ent.getKey(), ent.getValue()); } private void drawAllEdges(UGraphic ug) { for (Entry ent : edges.entrySet()) { final Link link = ent.getKey(); if (link.isInvis()) continue; drawSingleEdge(ug, link, ent.getValue()); } } private void drawSingleCluster(UGraphic ug, IGroup group, ElkNode elkNode) { final XPoint2D corner = getPosition(elkNode); final URectangle rect = new URectangle(elkNode.getWidth(), elkNode.getHeight()); PackageStyle packageStyle = group.getPackageStyle(); final ISkinParam skinParam = diagram.getSkinParam(); if (packageStyle == null) packageStyle = skinParam.packageStyle(); final UmlDiagramType umlDiagramType = diagram.getUmlDiagramType(); final Style style = Cluster.getDefaultStyleDefinition(umlDiagramType.getStyleName(), group.getUSymbol()) .getMergedStyle(skinParam.getCurrentStyleBuilder()); final double shadowing = style.value(PName.Shadowing).asDouble(); final UStroke stroke = Cluster.getStrokeInternal(group, style); HColor backColor = getBackColor(umlDiagramType); backColor = Cluster.getBackColor(backColor, group.getStereotype(), umlDiagramType.getStyleName(), group.getUSymbol(), skinParam.getCurrentStyleBuilder(), skinParam.getIHtmlColorSet()); final double roundCorner = style.value(PName.RoundCorner).asDouble(); // final double roundCorner = group.getUSymbol() == null ? 0 // : group.getUSymbol().getSkinParameter().getRoundCorner(skinParam, group.getStereotype()); final TextBlock ztitle = getTitleBlock(group); final TextBlock zstereo = TextBlockUtils.empty(0, 0); final ClusterPosition clusterPosition = new ClusterPosition(0, 0, elkNode.getWidth(), elkNode.getHeight()); final ClusterDecoration decoration = new ClusterDecoration(packageStyle, group.getUSymbol(), ztitle, zstereo, clusterPosition, stroke); final HColor borderColor = HColors.BLACK; decoration.drawU(ug.apply(new UTranslate(corner)), backColor, borderColor, shadowing, roundCorner, skinParam.getHorizontalAlignment(AlignmentParam.packageTitleAlignment, null, false, null), skinParam.getStereotypeAlignment(), 0); // // Print a simple rectangle right now // ug.apply(HColorUtils.BLACK).apply(new UStroke(1.5)).apply(new UTranslate(corner)).draw(rect); } private TextBlock getTitleBlock(IGroup g) { final Display label = g.getDisplay(); if (label == null) return TextBlockUtils.empty(0, 0); final ISkinParam skinParam = diagram.getSkinParam(); final FontConfiguration fontConfiguration = g.getFontConfigurationForTitle(skinParam); return label.create(fontConfiguration, HorizontalAlignment.CENTER, skinParam); } private HColor getBackColor(UmlDiagramType umlDiagramType) { return null; } private void drawSingleNode(UGraphic ug, ILeaf leaf, ElkNode elkNode) { final IEntityImage image = printEntityInternal(leaf); // Retrieve coord from ELK final XPoint2D corner = getPosition(elkNode); // Print the node image at right coord image.drawU(ug.apply(new UTranslate(corner))); } private void drawSingleEdge(UGraphic ug, Link link, ElkEdge edge) { // Unfortunately, we have to translate "edge" in its own "cluster" coordinate final XPoint2D translate = getPosition(edge.getContainingNode()); final double magicY2 = 0; final IEntity dest = link.getEntity2(); if (dest.getUSymbol() instanceof USymbolFolder) { // System.err.println("dest=" + dest); // final IEntityImage image = printEntityInternal((ILeaf) dest); // System.err.println("image=" + image); } final ElkPath elkPath = new ElkPath(diagram, SName.classDiagram, link, edge, getLabel(link), getQuantifier(link, 1), getQuantifier(link, 2), magicY2); elkPath.drawU(ug.apply(new UTranslate(translate))); } public XDimension2D calculateDimension(StringBounder stringBounder) { if (minMax == null) throw new UnsupportedOperationException(); return minMax.getDimension(); } public HColor getBackcolor() { return null; } } private Collection getUnpackagedEntities() { final List result = new ArrayList<>(); for (ILeaf ent : diagram.getLeafsvalues()) if (diagram.getEntityFactory().getRootGroup() == ent.getParentContainer()) result.add(ent); return result; } private ElkNode getElkNode(final IEntity entity) { ElkNode node = nodes.get(entity); if (node == null) node = clusters.get(entity); return node; } @Override public ImageData createFile(OutputStream os, List dotStrings, FileFormatOption fileFormatOption) throws IOException { // https://www.eclipse.org/forums/index.php/t/1095737/ try { final ElkNode root = ElkGraphUtil.createGraph(); root.setProperty(CoreOptions.DIRECTION, Direction.DOWN); root.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN); printAllSubgroups(root, diagram.getRootGroup()); printEntities(root, getUnpackagedEntities()); manageAllEdges(); new RecursiveGraphLayoutEngine().layout(root, new NullElkProgressMonitor()); final MinMax minMax = TextBlockUtils.getMinMax(new Drawing(null), stringBounder, false); final TextBlock drawable = new Drawing(minMax); return diagram.createImageBuilder(fileFormatOption) // .drawable(drawable) // .write(os); // } catch (Throwable e) { UmlDiagram.exportDiagramError(os, e, fileFormatOption, diagram.seed(), diagram.getMetadata(), diagram.getFlashData(), getFailureText3(e)); return ImageDataSimple.error(); } } private void printAllSubgroups(ElkNode cluster, IGroup group) { for (IGroup g : diagram.getChildrenGroups(group)) { if (g.isRemoved()) { continue; } if (diagram.isEmpty(g) && g.getGroupType() == GroupType.PACKAGE) { final ISkinParam skinParam = diagram.getSkinParam(); final EntityFactory entityFactory = diagram.getEntityFactory(); final ILeaf folder = entityFactory.createLeafForEmptyGroup(g, skinParam); System.err.println("STILL IN PROGRESS"); // printEntityNew(folder); } else { // We create the "cluster" in ELK for this group final ElkNode elkCluster = ElkGraphUtil.createNode(cluster); elkCluster.setProperty(CoreOptions.DIRECTION, Direction.DOWN); elkCluster.setProperty(CoreOptions.PADDING, new ElkPadding(40, 15, 15, 15)); // Not sure this is usefull to put a label on a "cluster" final ElkLabel label = ElkGraphUtil.createLabel(elkCluster); label.setText("C"); // We need it anyway to recurse up to the real "root" this.clusters.put(g, elkCluster); printSingleGroup(g); } } } private void printSingleGroup(IGroup g) { if (g.getGroupType() == GroupType.CONCURRENT_STATE) return; this.printEntities(clusters.get(g), g.getLeafsDirect()); printAllSubgroups(clusters.get(g), g); } private void printEntities(ElkNode parent, Collection entities) { // Convert all "leaf" to ELK node for (ILeaf ent : entities) { if (ent.isRemoved()) continue; manageSingleNode(parent, ent); } } private void manageAllEdges() { // Convert all "link" to ELK edge for (final Link link : diagram.getLinks()) manageSingleEdge(link); } private void manageSingleNode(final ElkNode root, ILeaf leaf) { final IEntityImage image = printEntityInternal(leaf); // Expected dimension of the node final XDimension2D dimension = image.calculateDimension(stringBounder); // Here, we try to tell ELK to use this dimension as node dimension final ElkNode node = ElkGraphUtil.createNode(root); node.setDimensions(dimension.getWidth(), dimension.getHeight()); // There is no real "label" here // We just would like to force node dimension final ElkLabel label = ElkGraphUtil.createLabel(node); label.setText("X"); // I don't know why we have to do this hack, but somebody has to fix it final double VERY_STRANGE_OFFSET = 10; label.setDimensions(dimension.getWidth(), dimension.getHeight() - VERY_STRANGE_OFFSET); // No idea of what we are doing here :-) label.setProperty(CoreOptions.NODE_LABELS_PLACEMENT, EnumSet.of(NodeLabelPlacement.INSIDE, NodeLabelPlacement.H_CENTER, NodeLabelPlacement.V_CENTER)); // This padding setting have no impact ? // label.setProperty(CoreOptions.NODE_LABELS_PADDING, new ElkPadding(100.0)); // final EnumSet constraints = // EnumSet.of(SizeConstraint.NODE_LABELS); // node.setProperty(CoreOptions.NODE_SIZE_CONSTRAINTS, constraints); // node.setProperty(CoreOptions.NODE_SIZE_OPTIONS, // EnumSet.noneOf(SizeOptions.class)); // Let's store this nodes.put(leaf, node); } private void manageSingleEdge(final Link link) { final ElkNode node1 = getElkNode(link.getEntity1()); final ElkNode node2 = getElkNode(link.getEntity2()); final ElkEdge edge = ElkGraphUtil.createSimpleEdge(node1, node2); final TextBlock labelLink = getLabel(link); if (labelLink != null) { final ElkLabel edgeLabel = ElkGraphUtil.createLabel(edge); final XDimension2D dim = labelLink.calculateDimension(stringBounder); edgeLabel.setText("X"); edgeLabel.setDimensions(dim.getWidth(), dim.getHeight()); // Duplicated, with qualifier, but who cares? edge.setProperty(CoreOptions.EDGE_LABELS_INLINE, true); // edge.setProperty(CoreOptions.EDGE_TYPE, EdgeType.ASSOCIATION); } if (link.getQuantifier1() != null) { final ElkLabel edgeLabel = ElkGraphUtil.createLabel(edge); final XDimension2D dim = getQuantifier(link, 1).calculateDimension(stringBounder); // Nasty trick, we store the kind of label in the text edgeLabel.setText("1"); edgeLabel.setDimensions(dim.getWidth(), dim.getHeight()); edgeLabel.setProperty(CoreOptions.EDGE_LABELS_PLACEMENT, EdgeLabelPlacement.TAIL); // Duplicated, with main label, but who cares? edge.setProperty(CoreOptions.EDGE_LABELS_INLINE, true); // edge.setProperty(CoreOptions.EDGE_TYPE, EdgeType.ASSOCIATION); } if (link.getQuantifier2() != null) { final ElkLabel edgeLabel = ElkGraphUtil.createLabel(edge); final XDimension2D dim = getQuantifier(link, 2).calculateDimension(stringBounder); // Nasty trick, we store the kind of label in the text edgeLabel.setText("2"); edgeLabel.setDimensions(dim.getWidth(), dim.getHeight()); edgeLabel.setProperty(CoreOptions.EDGE_LABELS_PLACEMENT, EdgeLabelPlacement.HEAD); // Duplicated, with main label, but who cares? edge.setProperty(CoreOptions.EDGE_LABELS_INLINE, true); // edge.setProperty(CoreOptions.EDGE_TYPE, EdgeType.ASSOCIATION); } edges.put(link, edge); } static private List getFailureText3(Throwable exception) { Logme.error(exception); final List strings = new ArrayList<>(); strings.add("An error has occured : " + exception); final String quote = StringUtils.rot(QuoteUtils.getSomeQuote()); strings.add("" + quote); strings.add(" "); GraphvizCrash.addProperties(strings); strings.add(" "); strings.add("Sorry, ELK intregration is really alpha feature..."); strings.add(" "); strings.add("You should send this diagram and this image to plantuml@gmail.com or"); strings.add("post to http://plantuml.com/qa to solve this issue."); strings.add(" "); return strings; } private Bibliotekon getBibliotekon() { return dotStringFactory.getBibliotekon(); } private IEntityImage printEntityInternal(ILeaf ent) { if (ent.isRemoved()) throw new IllegalStateException(); if (ent.getSvekImage() == null) { final ISkinParam skinParam = diagram.getSkinParam(); if (skinParam.sameClassWidth()) System.err.println("NOT YET IMPLEMENED"); return GeneralImageBuilder.createEntityImageBlock(ent, skinParam, diagram.isHideEmptyDescriptionForState(), diagram, getBibliotekon(), null, diagram.getUmlDiagramType(), diagram.getLinks()); } return ent.getSvekImage(); } }