/* ======================================================================== * PlantUML : a free UML diagram generator * ======================================================================== * * (C) Copyright 2009-2024, 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 * Contribution : Hisashi Miyashita * * */ package net.sourceforge.plantuml.svek; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import net.sourceforge.plantuml.abel.CucaNote; import net.sourceforge.plantuml.abel.Entity; import net.sourceforge.plantuml.abel.EntityPosition; import net.sourceforge.plantuml.abel.Together; import net.sourceforge.plantuml.cucadiagram.ICucaDiagram; import net.sourceforge.plantuml.decoration.symbol.USymbol; import net.sourceforge.plantuml.decoration.symbol.USymbols; import net.sourceforge.plantuml.dot.GraphvizVersion; import net.sourceforge.plantuml.klimt.UGroupType; import net.sourceforge.plantuml.klimt.UStroke; import net.sourceforge.plantuml.klimt.UTranslate; import net.sourceforge.plantuml.klimt.color.ColorType; import net.sourceforge.plantuml.klimt.color.Colors; import net.sourceforge.plantuml.klimt.color.HColor; import net.sourceforge.plantuml.klimt.color.HColorSet; import net.sourceforge.plantuml.klimt.color.HColors; import net.sourceforge.plantuml.klimt.drawing.UGraphic; import net.sourceforge.plantuml.klimt.font.StringBounder; import net.sourceforge.plantuml.klimt.geom.Moveable; import net.sourceforge.plantuml.klimt.geom.RectangleArea; import net.sourceforge.plantuml.klimt.geom.XDimension2D; import net.sourceforge.plantuml.klimt.geom.XPoint2D; import net.sourceforge.plantuml.klimt.shape.TextBlock; import net.sourceforge.plantuml.klimt.shape.UComment; import net.sourceforge.plantuml.klimt.shape.ULine; import net.sourceforge.plantuml.skin.AlignmentParam; import net.sourceforge.plantuml.skin.UmlDiagramType; import net.sourceforge.plantuml.stereo.Stereotype; import net.sourceforge.plantuml.style.ISkinParam; import net.sourceforge.plantuml.style.PName; import net.sourceforge.plantuml.style.SName; import net.sourceforge.plantuml.style.Style; import net.sourceforge.plantuml.style.StyleBuilder; import net.sourceforge.plantuml.style.StyleSignatureBasic; import net.sourceforge.plantuml.svek.image.EntityImageNoteLink; import net.sourceforge.plantuml.svek.image.EntityImageState; import net.sourceforge.plantuml.svek.image.EntityImageStateCommon; import net.sourceforge.plantuml.url.Url; import net.sourceforge.plantuml.utils.Position; public class Cluster implements Moveable { // /* private */ static final String RANK_SAME = "same"; /* private */ static final String RANK_SOURCE = "source"; /* private */ static final String RANK_SINK = "sink"; public final static String CENTER_ID = "za"; private final Cluster parentCluster; private final Entity group; private final List nodes = new ArrayList<>(); private final List children = new ArrayList<>(); private final int color; private final int colorTitle; private final int colorNoteTop; private final int colorNoteBottom; private final ISkinParam skinParam; protected final ICucaDiagram diagram; private ClusterHeader clusterHeader; private XPoint2D xyTitle; private XPoint2D xyNoteTop; private XPoint2D xyNoteBottom; private RectangleArea rectangleArea; public void moveSvek(double deltaX, double deltaY) { if (this.xyNoteTop != null) this.xyNoteTop = this.xyNoteTop.move(deltaX, deltaY); if (this.xyNoteBottom != null) this.xyNoteBottom = this.xyNoteBottom.move(deltaX, deltaY); if (this.xyTitle != null) this.xyTitle = this.xyTitle.move(deltaX, deltaY); if (this.rectangleArea != null) this.rectangleArea = this.rectangleArea.move(deltaX, deltaY); } private Set entityPositionsExceptNormal() { final Set result = EnumSet.noneOf(EntityPosition.class); for (SvekNode sh : nodes) if (sh.getEntityPosition() != EntityPosition.NORMAL) result.add(sh.getEntityPosition()); return Collections.unmodifiableSet(result); } public Cluster(ICucaDiagram diagram, ColorSequence colorSequence, ISkinParam skinParam, Entity root) { this(diagram, null, colorSequence, skinParam, root); } private Cluster(ICucaDiagram diagram, Cluster parentCluster, ColorSequence colorSequence, ISkinParam skinParam, Entity group) { if (group == null) throw new IllegalStateException(); this.parentCluster = parentCluster; this.group = group; this.diagram = diagram; this.color = colorSequence.getValue(); this.colorTitle = colorSequence.getValue(); this.colorNoteTop = colorSequence.getValue(); this.colorNoteBottom = colorSequence.getValue(); this.skinParam = group.getColors().mute(skinParam); } @Override public String toString() { return super.toString() + " " + group; } public final Cluster getParentCluster() { return parentCluster; } public void addNode(SvekNode node) { this.nodes.add(Objects.requireNonNull(node)); node.setCluster(this); } public final List getNodes() { return Collections.unmodifiableList(nodes); } public final List getNodes(EnumSet position) { final List result = new ArrayList<>(); for (SvekNode node : nodes) if (position.contains(node.getEntityPosition())) result.add(node); return Collections.unmodifiableList(result); } private List getNodesOrderedTop(Collection lines) { final List firsts = new ArrayList<>(); final Map shs = new HashMap(); for (final Iterator it = nodes.iterator(); it.hasNext();) { final SvekNode node = it.next(); shs.put(node.getUid(), node); } for (SvekLine l : lines) if (l.isInverted()) { final SvekNode sh = shs.get(l.getStartUidPrefix()); if (sh != null && isNormalPosition(sh)) firsts.add(0, sh); } return firsts; } private boolean isNormalPosition(final SvekNode sh) { return sh.getEntityPosition() == EntityPosition.NORMAL; } private List getNodesOrderedWithoutTop(Collection lines) { final List all = new ArrayList<>(nodes); final Map shs = new HashMap(); for (final Iterator it = all.iterator(); it.hasNext();) { final SvekNode sh = it.next(); if (isNormalPosition(sh) == false) { it.remove(); continue; } shs.put(sh.getUid(), sh); } for (SvekLine l : lines) if (l.isInverted()) { final SvekNode sh = shs.get(l.getStartUidPrefix()); if (sh != null) all.remove(sh); } return all; } public final List getChildren() { return Collections.unmodifiableList(children); } public Cluster createChild(ClusterHeader clusterHeader, ColorSequence colorSequence, ISkinParam skinParam, Entity g) { final Cluster child = new Cluster(diagram, this, colorSequence, skinParam, g); child.clusterHeader = clusterHeader; this.children.add(child); return child; } public final Set getGroups() { return Collections.singleton(group); } final Entity getGroup() { return group; } public final int getTitleAndAttributeWidth() { return clusterHeader.getTitleAndAttributeWidth(); } public final int getTitleAndAttributeHeight() { return clusterHeader.getTitleAndAttributeHeight(); } public RectangleArea getRectangleArea() { return rectangleArea; } public void setTitlePosition(XPoint2D pos) { this.xyTitle = pos; } public void setNoteTopPosition(XPoint2D pos) { this.xyNoteTop = pos; } public void setNoteBottomPosition(XPoint2D pos) { this.xyNoteBottom = pos; } static public StyleSignatureBasic getDefaultStyleDefinition(SName diagramStyleName, USymbol symbol) { if (diagramStyleName == SName.stateDiagram) return StyleSignatureBasic.of(SName.root, SName.element, SName.stateDiagram, SName.state, SName.group); if (symbol == null) return StyleSignatureBasic.of(SName.root, SName.element, diagramStyleName, SName.group); return StyleSignatureBasic.of(SName.root, SName.element, diagramStyleName, SName.group, symbol.getSName()); } public void drawU(UGraphic ug, UmlDiagramType umlDiagramType, ISkinParam skinParam2unused) { if (group.isHidden()) return; if (diagram.getPragma().useKermor()) { if (xyNoteTop != null) getCucaNote(Position.TOP).drawU(ug.apply(new UTranslate(xyNoteTop))); if (xyNoteBottom != null) getCucaNote(Position.BOTTOM).drawU(ug.apply(new UTranslate(xyNoteBottom))); } final String fullName = group.getName(); if (fullName.startsWith("##") == false) ug.draw(new UComment("cluster " + fullName)); final USymbol uSymbol = group.getUSymbol() == null ? USymbols.PACKAGE : group.getUSymbol(); Style style = getDefaultStyleDefinition(umlDiagramType.getStyleName(), uSymbol) .withTOBECHANGED(group.getStereotype()).getMergedStyle(skinParam.getCurrentStyleBuilder()); final double shadowing = style.value(PName.Shadowing).asDouble(); HColor borderColor; if (group.getColors().getColor(ColorType.LINE) != null) borderColor = group.getColors().getColor(ColorType.LINE); else borderColor = style.value(PName.LineColor).asColor(skinParam.getIHtmlColorSet()); final double rounded = style.value(PName.RoundCorner).asDouble(); final double diagonalCorner = style.value(PName.DiagonalCorner).asDouble(); ug.startGroup(Collections.singletonMap(UGroupType.ID, "cluster_" + fullName)); final Url url = group.getUrl99(); if (url != null) ug.startUrl(url); try { if (entityPositionsExceptNormal().size() > 0) manageEntryExitPoint(ug.getStringBounder()); if (skinParam.useSwimlanes(umlDiagramType)) { drawSwinLinesState(ug, borderColor); return; } final boolean isState = umlDiagramType == UmlDiagramType.STATE; if (isState && group.getUSymbol() == null) { drawUState(ug, umlDiagramType, rounded, shadowing); return; } PackageStyle packageStyle = group.getPackageStyle(); if (packageStyle == null) packageStyle = skinParam.packageStyle(); final UStroke stroke = getStrokeInternal(group, style); HColor backColor = getBackColor(umlDiagramType, style); backColor = getBackColor(backColor, group.getStereotype(), umlDiagramType.getStyleName(), group.getUSymbol(), skinParam.getCurrentStyleBuilder(), skinParam.getIHtmlColorSet()); final ClusterDecoration decoration = new ClusterDecoration(packageStyle, group.getUSymbol(), clusterHeader.getTitle(), clusterHeader.getStereo(), rectangleArea, stroke); decoration.drawU(ug, backColor, borderColor, shadowing, rounded, skinParam.getHorizontalAlignment(AlignmentParam.packageTitleAlignment, null, false, null), skinParam.getStereotypeAlignment(), diagonalCorner); } catch (Exception e) { e.printStackTrace(); } finally { if (url != null) ug.closeUrl(); ug.closeGroup(); } } EntityImageNoteLink getCucaNote(Position position) { final List notes = getGroup().getNotes(position); if (notes.size() == 0) return null; final CucaNote note = notes.get(0); return new EntityImageNoteLink(note.getDisplay(), note.getColors(), skinParam, skinParam.getCurrentStyleBuilder()); } static public UStroke getStrokeInternal(Entity group, Style style) { final Colors colors = group.getColors(); if (colors.getSpecificLineStroke() != null) return colors.getSpecificLineStroke(); return style.getStroke(); } void manageEntryExitPoint(StringBounder stringBounder) { final Collection insides = new ArrayList<>(); final List points = new ArrayList<>(); for (SvekNode sh : nodes) if (isNormalPosition(sh)) insides.add(sh.getRectangleArea()); else points.add(sh.getRectangleArea().getPointCenter()); for (Cluster in : children) insides.add(in.getRectangleArea()); final FrontierCalculator frontierCalculator = new FrontierCalculator(getRectangleArea(), insides, points, skinParam.getRankdir()); if (getTitleAndAttributeWidth() > 0 && getTitleAndAttributeHeight() > 0) frontierCalculator.ensureMinWidth(getTitleAndAttributeWidth() + 10); this.rectangleArea = frontierCalculator.getSuggestedPosition(); final double widthTitle = clusterHeader.getTitle().calculateDimension(stringBounder).getWidth(); final double minX = rectangleArea.getMinX(); final double minY = rectangleArea.getMinY(); this.xyTitle = new XPoint2D(minX + ((rectangleArea.getWidth() - widthTitle) / 2), minY + IEntityImage.MARGIN); } private void drawSwinLinesState(UGraphic ug, HColor borderColor) { clusterHeader.getTitle().drawU(ug.apply(UTranslate.dx(xyTitle.x))); final ULine line = ULine.vline(rectangleArea.getHeight()); ug = ug.apply(borderColor); ug.apply(UTranslate.dx(rectangleArea.getMinX())).draw(line); ug.apply(UTranslate.dx(rectangleArea.getMaxX())).draw(line); } // GroupPngMakerState private void drawUState(UGraphic ug, UmlDiagramType umlDiagramType, double rounded, double shadowing) { final XDimension2D total = rectangleArea.getDimension(); final double suppY = clusterHeader.getTitle().calculateDimension(ug.getStringBounder()).getHeight() + IEntityImage.MARGIN; HColor borderColor = group.getColors().getColor(ColorType.LINE); if (borderColor == null) borderColor = EntityImageStateCommon.getStyleState(group, skinParam).value(PName.LineColor) .asColor(skinParam.getIHtmlColorSet()); HColor backColor = group.getColors().getColor(ColorType.BACK); if (backColor == null) backColor = EntityImageStateCommon.getStyleState(group, skinParam).value(PName.BackGroundColor) .asColor(skinParam.getIHtmlColorSet()); final HColor imgBackcolor = EntityImageStateCommon.getStyleStateBody(group, skinParam) .value(PName.BackGroundColor).asColor(skinParam.getIHtmlColorSet()); final TextBlock attribute = ((Entity) group).getStateHeader(skinParam); final double attributeHeight = attribute.calculateDimension(ug.getStringBounder()).getHeight(); if (total.getWidth() == 0) { System.err.println("Cluster::drawUState issue"); return; } UStroke stroke = group.getColors().getSpecificLineStroke(); if (stroke == null) stroke = EntityImageStateCommon.getStyleState(group, skinParam).getStroke(); final RoundedContainer r = new RoundedContainer(total, suppY, attributeHeight + (attributeHeight > 0 ? IEntityImage.MARGIN : 0), borderColor, backColor, imgBackcolor, stroke, rounded, shadowing); r.drawU(ug.apply(rectangleArea.getPosition())); clusterHeader.getTitle().drawU(ug.apply(new UTranslate(xyTitle))); if (attributeHeight > 0) attribute.drawU(ug.apply(new UTranslate(rectangleArea.getMinX() + IEntityImage.MARGIN, rectangleArea.getMinY() + suppY + IEntityImage.MARGIN / 2.0))); final Stereotype stereotype = group.getStereotype(); final boolean withSymbol = stereotype != null && stereotype.isWithOOSymbol(); if (withSymbol) EntityImageState.drawSymbol(ug.apply(borderColor), rectangleArea.getMaxX(), rectangleArea.getMaxY()); } public void setPosition(XPoint2D min, XPoint2D max) { this.rectangleArea = new RectangleArea(min, max); } // ::comment when CORE public boolean printCluster1(StringBuilder sb, Collection lines, StringBounder stringBounder) { final List tmp = getNodesOrderedTop(lines); if (tmp.size() == 0) return false; for (SvekNode node : tmp) node.appendShape(sb, stringBounder); return true; } private int togetherCounter = 0; private void printTogether(Together together, StringBuilder sb, List nodesOrderedWithoutTop, StringBounder stringBounder, Collection lines, DotMode dotMode, GraphvizVersion graphvizVersion, UmlDiagramType type) { sb.append("subgraph " + getClusterId() + "t" + togetherCounter + " {\n"); for (SvekNode node : nodesOrderedWithoutTop) if (node.getTogether() == together) node.appendShape(sb, stringBounder); for (Cluster child : children) if (child.group.getTogether() == together) child.printInternal(sb, lines, stringBounder, dotMode, graphvizVersion, type); sb.append("}\n"); togetherCounter++; } public SvekNode printCluster2(StringBuilder sb, Collection lines, StringBounder stringBounder, DotMode dotMode, GraphvizVersion graphvizVersion, UmlDiagramType type) { SvekNode added = null; final Collection togethers = new LinkedHashSet<>(); final List nodesOrderedWithoutTop = getNodesOrderedWithoutTop(lines); for (SvekNode node : nodesOrderedWithoutTop) { final Together together = node.getTogether(); if (together == null) node.appendShape(sb, stringBounder); else togethers.add(together); added = node; } for (Cluster child : children) if (child.group.getTogether() != null) togethers.add(child.group.getTogether()); for (Together together : togethers) printTogether(together, sb, nodesOrderedWithoutTop, stringBounder, lines, dotMode, graphvizVersion, type); if (skinParam.useRankSame() && dotMode != DotMode.NO_LEFT_RIGHT_AND_XLABEL && graphvizVersion.ignoreHorizontalLinks() == false) appendRankSame(sb, lines); for (Cluster child : children) if (child.group.getTogether() == null) child.printInternal(sb, lines, stringBounder, dotMode, graphvizVersion, type); return added; } public void printCluster3_forKermor(StringBuilder sb, Collection lines, StringBounder stringBounder, DotMode dotMode, GraphvizVersion graphvizVersion, UmlDiagramType type) { final List tmp = getNodes(EntityPosition.getNormals()); if (tmp.size() == 0) { sb.append(getClusterId() + "empty [shape=point,label=\"\"];"); SvekUtils.println(sb); } else { for (SvekNode node : tmp) node.appendShape(sb, stringBounder); } for (Cluster child : getChildren()) child.printInternal(sb, lines, stringBounder, dotMode, graphvizVersion, type); } private void printInternal(StringBuilder sb, Collection lines, StringBounder stringBounder, DotMode dotMode, GraphvizVersion graphvizVersion, UmlDiagramType type) { new ClusterDotString(this, skinParam).printInternal(sb, lines, stringBounder, dotMode, graphvizVersion, type); } private void appendRankSame(StringBuilder sb, Collection lines) { for (String same : getRankSame(lines)) { sb.append(same); SvekUtils.println(sb); } } // ::done private Set getRankSame(Collection lines) { final Set rankSame = new HashSet<>(); for (SvekLine l : lines) { if (l.hasEntryPoint()) continue; final String startUid = l.getStartUidPrefix(); final String endUid = l.getEndUidPrefix(); if (isInCluster(startUid) && isInCluster(endUid)) { final String same = l.rankSame(); if (same != null) rankSame.add(same); } } return rankSame; } private boolean isInCluster(String uid) { for (SvekNode node : nodes) if (node.getUid().equals(uid)) return true; return false; } public String getClusterId() { return "cluster" + color; } static String getSpecialPointId(Entity group) { return CENTER_ID + group.getUid(); } String getMinPoint(UmlDiagramType type) { if (skinParam.useSwimlanes(type)) return "minPoint" + color; return null; } String getMaxPoint(UmlDiagramType type) { if (skinParam.useSwimlanes(type)) return "maxPoint" + color; return null; } public boolean isLabel() { return getTitleAndAttributeHeight() > 0 && getTitleAndAttributeWidth() > 0; } int getColor() { return color; } int getTitleColor() { return colorTitle; } private final HColor getBackColor(UmlDiagramType umlDiagramType, Style style) { if (group.isRoot()) return null; final HColor result = group.getColors().getColor(ColorType.BACK); if (result != null) return result; return style.value(PName.BackGroundColor).asColor(skinParam.getIHtmlColorSet()); } boolean isClusterOf(Entity ent) { if (ent.isGroup() == false) return false; return group == ent; } public static HColor getBackColor(HColor backColor, Stereotype stereotype, SName styleName, USymbol symbol, StyleBuilder styleBuilder, HColorSet colorSet) { final Style style = getDefaultStyleDefinition(styleName, symbol).getMergedStyle(styleBuilder); if (backColor == null) backColor = style.value(PName.BackGroundColor).asColor(colorSet); if (backColor == null || backColor.equals(HColors.transparent())) backColor = HColors.transparent(); return backColor; } // double checkFolderPosition(XPoint2D pt, StringBounder stringBounder) { // if (getClusterPosition().isPointJustUpper(pt)) { // // final XDimension2D dimTitle = clusterHeader.getTitle().calculateDimension(stringBounder); // // if (pt.getX() < getClusterPosition().getMinX() + dimTitle.getWidth()) // return 0; // // return getClusterPosition().getMinY() - pt.getY() + dimTitle.getHeight(); // } // return 0; // } public final int getColorNoteTop() { return colorNoteTop; } public final int getColorNoteBottom() { return colorNoteBottom; } public XDimension2D getTitleDimension(StringBounder stringBounder) { return clusterHeader.getTitle().calculateDimension(stringBounder); } // public XPoint2D projection(double x, double y) { // final double v1 = Math.abs(minX - x); // final double v2 = Math.abs(maxX - x); // final double v3 = Math.abs(minY - y); // final double v4 = Math.abs(maxY - y); // if (v1 <= v2 && v1 <= v3 && v1 <= v4) { // return new XPoint2D(minX, y); // } // if (v2 <= v1 && v2 <= v3 && v2 <= v4) { // return new XPoint2D(maxX, y); // } // if (v3 <= v1 && v3 <= v2 && v3 <= v4) { // return new XPoint2D(x, minY); // } // if (v4 <= v1 && v4 <= v1 && v4 <= v3) { // return new XPoint2D(x, maxY); // } // throw new IllegalStateException(); // } }