| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.batchee.tools.maven; |
| |
| |
| import edu.uci.ics.jung.algorithms.layout.AbstractLayout; |
| import edu.uci.ics.jung.algorithms.layout.CircleLayout; |
| import edu.uci.ics.jung.algorithms.layout.FRLayout; |
| import edu.uci.ics.jung.algorithms.layout.KKLayout; |
| import edu.uci.ics.jung.algorithms.layout.Layout; |
| import edu.uci.ics.jung.algorithms.layout.SpringLayout; |
| import edu.uci.ics.jung.algorithms.shortestpath.DijkstraDistance; |
| import edu.uci.ics.jung.algorithms.shortestpath.Distance; |
| import edu.uci.ics.jung.algorithms.shortestpath.UnweightedShortestPath; |
| import edu.uci.ics.jung.graph.DirectedSparseGraph; |
| import edu.uci.ics.jung.graph.Graph; |
| import edu.uci.ics.jung.graph.util.Context; |
| import edu.uci.ics.jung.graph.util.Pair; |
| import edu.uci.ics.jung.visualization.Layer; |
| import edu.uci.ics.jung.visualization.RenderContext; |
| import edu.uci.ics.jung.visualization.VisualizationViewer; |
| import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse; |
| import edu.uci.ics.jung.visualization.decorators.EdgeShape; |
| import edu.uci.ics.jung.visualization.decorators.ToStringLabeller; |
| import edu.uci.ics.jung.visualization.renderers.BasicEdgeLabelRenderer; |
| import edu.uci.ics.jung.visualization.renderers.Renderer; |
| import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator; |
| import org.apache.batchee.container.jsl.ExecutionElement; |
| import org.apache.batchee.container.jsl.JobModelResolver; |
| import org.apache.batchee.container.jsl.TransitionElement; |
| import org.apache.batchee.container.navigator.JobNavigator; |
| import org.apache.batchee.jaxb.End; |
| import org.apache.batchee.jaxb.Fail; |
| import org.apache.batchee.jaxb.Flow; |
| import org.apache.batchee.jaxb.JSLJob; |
| import org.apache.batchee.jaxb.Next; |
| import org.apache.batchee.jaxb.Split; |
| import org.apache.batchee.jaxb.Step; |
| import org.apache.batchee.jaxb.Stop; |
| import org.apache.commons.collections15.Transformer; |
| import org.apache.commons.collections15.functors.ConstantTransformer; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.apache.maven.plugins.annotations.Mojo; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.codehaus.plexus.util.IOUtil; |
| |
| import javax.imageio.ImageIO; |
| import javax.swing.JFrame; |
| import java.awt.Color; |
| import java.awt.Component; |
| import java.awt.Dimension; |
| import java.awt.FontMetrics; |
| import java.awt.Graphics2D; |
| import java.awt.GridLayout; |
| import java.awt.Paint; |
| import java.awt.Point; |
| import java.awt.Rectangle; |
| import java.awt.RenderingHints; |
| import java.awt.Shape; |
| import java.awt.event.WindowAdapter; |
| import java.awt.event.WindowEvent; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.Ellipse2D; |
| import java.awt.geom.Point2D; |
| import java.awt.image.AffineTransformOp; |
| import java.awt.image.BufferedImage; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.CountDownLatch; |
| |
| @Mojo(name = "diagram") |
| public class DiagramMojo extends AbstractMojo { |
| @Parameter(property = "batchee.path", required = true) |
| protected String path; |
| |
| @Parameter(property = "batchee.failIfMissing", defaultValue = "false") |
| protected boolean failIfMissing; |
| |
| @Parameter(property = "batchee.viewer", defaultValue = "false") |
| protected boolean view; |
| |
| @Parameter(property = "batchee.width", defaultValue = "640") |
| protected int width; |
| |
| @Parameter(property = "batchee.height", defaultValue = "480") |
| protected int height; |
| |
| @Parameter(property = "batchee.adjust", defaultValue = "false") |
| protected boolean adjust; |
| |
| @Parameter(property = "batchee.outputDir", defaultValue = "${project.build.directory}/batchee-diagram/") |
| protected File output; |
| |
| @Parameter(property = "batchee.format", defaultValue = "png") |
| protected String format; |
| |
| @Parameter(property = "batchee.outputFileName") |
| protected String outputFileName; |
| |
| @Parameter(property = "batchee.rotateEdges", defaultValue = "true") |
| protected boolean rotateEdges; |
| |
| // level, spring[distant],kk[unweight|dijstra],circle |
| @Parameter(property = "batchee.layout", defaultValue = "level") |
| protected String layout; |
| |
| @Override |
| public void execute() throws MojoExecutionException, MojoFailureException { |
| final String content = slurp(validInput()); |
| |
| final JSLJob job = new JobModelResolver().resolveModel(content); |
| |
| final List<ExecutionElement> executionElements = job.getExecutionElements(); |
| if (executionElements == null) { |
| getLog().warn("No step found, no diagram will be generated."); |
| return; |
| } |
| |
| final Diagram diagram = new Diagram(job.getId()); |
| visitBatch(job, diagram); |
| |
| draw(diagram); |
| } |
| |
| private void visitBatch(final JSLJob job, final Diagram diagram) throws MojoExecutionException { |
| final Map<String, Node> nodes = new HashMap<String, Node>(); |
| |
| String first = null; |
| try { |
| first = new JobNavigator(job).getFirstExecutionElement(null).getId(); |
| } catch (final Exception e) { |
| // no-op |
| } |
| |
| // create nodes |
| final List<ExecutionElement> executionElements = job.getExecutionElements(); |
| final Collection<ExecutionElement> allElements = new HashSet<ExecutionElement>(); |
| initNodes(diagram, nodes, allElements, executionElements); |
| |
| // create edges |
| for (final ExecutionElement element : allElements) { |
| final String id = element.getId(); |
| final Node source = nodes.get(id); |
| if (id.equals(first)) { |
| source.root(); |
| } |
| |
| if (Step.class.isInstance(element)) { |
| final String next = Step.class.cast(element).getNextFromAttribute(); |
| if (next != null) { |
| final Node target = addNodeIfMissing(diagram, nodes, next, Node.Type.STEP); |
| diagram.addEdge(new Edge("next"), source, target); |
| } |
| } |
| |
| for (final TransitionElement transitionElement : element.getTransitionElements()) { |
| if (Stop.class.isInstance(transitionElement)) { |
| final Stop stop = Stop.class.cast(transitionElement); |
| |
| final String restart = stop.getRestart(); |
| if (restart != null) { |
| final Node target = addNodeIfMissing(diagram, nodes, restart, Node.Type.STEP); |
| diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target); |
| } |
| |
| final String exitStatus = stop.getRestart(); |
| if (exitStatus != null) { |
| final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK); |
| diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target); |
| } |
| } else if (Fail.class.isInstance(transitionElement)) { |
| final Fail fail = Fail.class.cast(transitionElement); |
| final String exitStatus = fail.getExitStatus(); |
| final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK); |
| diagram.addEdge(new Edge("fail(" + fail.getOn() + ")"), source, target); |
| } else if (End.class.isInstance(transitionElement)) { |
| final End end = End.class.cast(transitionElement); |
| final String exitStatus = end.getExitStatus(); |
| final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK); |
| diagram.addEdge(new Edge("end(" + end.getOn() + ")"), source, target); |
| } else if (Next.class.isInstance(transitionElement)) { |
| final Next end = Next.class.cast(transitionElement); |
| final String to = end.getTo(); |
| final Node target = addNodeIfMissing(diagram, nodes, to, Node.Type.STEP); |
| diagram.addEdge(new Edge("next(" + end.getOn() + ")"), source, target); |
| } else { |
| getLog().warn("Unknown next element: " + transitionElement); |
| } |
| } |
| } |
| } |
| |
| private void initNodes(final Diagram diagram, final Map<String, Node> nodes, |
| final Collection<ExecutionElement> allElements, final Collection<ExecutionElement> executionElements) { |
| for (final ExecutionElement element : executionElements) { |
| final String id = element.getId(); |
| allElements.add(element); |
| |
| addNodeIfMissing(diagram, nodes, id, Node.Type.STEP); |
| |
| if (Split.class.isInstance(element)) { |
| final Split split = Split.class.cast(element); |
| final List<Flow> flows = split.getFlows(); |
| for (final Flow flow : flows) { |
| initNodes(diagram, nodes, allElements, flow.getExecutionElements()); |
| } |
| } else if (Flow.class.isInstance(element)) { |
| initNodes(diagram, nodes, allElements, Flow.class.cast(element).getExecutionElements()); |
| } // else if step or decision -> ok |
| } |
| } |
| |
| private static Node addNodeIfMissing(final Diagram diagram, final Map<String, Node> nodes, final String id, final Node.Type type) { |
| Node node = nodes.get(id); |
| if (node == null) { |
| node = new Node(id, type); |
| nodes.put(id, node); |
| diagram.addVertex(node); |
| } |
| return node; |
| } |
| |
| private void draw(final Diagram diagram) throws MojoExecutionException { |
| final Layout<Node, Edge> diagramLayout = newLayout(diagram); |
| |
| final Dimension outputSize = new Dimension(width, height); |
| final VisualizationViewer<Node, Edge> viewer = new GraphViewer(diagramLayout, rotateEdges); |
| |
| if (LevelLayout.class.isInstance(diagramLayout)) { |
| LevelLayout.class.cast(diagramLayout).vertexShapeTransformer = viewer.getRenderContext().getVertexShapeTransformer(); |
| } |
| |
| diagramLayout.setSize(outputSize); |
| diagramLayout.reset(); |
| viewer.setPreferredSize(diagramLayout.getSize()); |
| viewer.setSize(diagramLayout.getSize()); |
| |
| // saving it too |
| if (!output.exists() && !output.mkdirs()) { |
| throw new MojoExecutionException("Can't create '" + output.getPath() + "'"); |
| } |
| saveView(diagramLayout.getSize(), outputSize, diagram.getName(), viewer); |
| |
| // viewing the window if necessary |
| if (view) { |
| final JFrame window = createWindow(viewer, diagram.getName()); |
| final CountDownLatch latch = new CountDownLatch(1); |
| window.setVisible(true); |
| window.addWindowListener(new WindowAdapter() { |
| @Override |
| public void windowClosed(WindowEvent e) { |
| super.windowClosed(e); |
| latch.countDown(); |
| } |
| }); |
| try { |
| latch.await(); |
| } catch (final InterruptedException e) { |
| getLog().error("can't await window close event", e); |
| } |
| } |
| } |
| |
| private Layout<Node, Edge> newLayout(final Diagram diagram) { |
| final Layout<Node, Edge> diagramLayout; |
| if (layout != null && layout.startsWith("spring")) { |
| diagramLayout = new SpringLayout<Node, Edge>(diagram, new ConstantTransformer(Integer.parseInt(config("spring", "100")))); |
| } else if (layout != null && layout.startsWith("kk")) { |
| Distance<Node> distance = new DijkstraDistance<Node, Edge>(diagram); |
| if (layout.endsWith("unweight")) { |
| distance = new UnweightedShortestPath<Node, Edge>(diagram); |
| } |
| diagramLayout = new KKLayout<Node, Edge>(diagram, distance); |
| } else if (layout != null && layout.equalsIgnoreCase("circle")) { |
| diagramLayout = new CircleLayout<Node, Edge>(diagram); |
| } else if (layout != null && layout.equalsIgnoreCase("fr")) { |
| diagramLayout = new FRLayout<Node, Edge>(diagram); |
| } else { |
| final LevelLayout levelLayout = new LevelLayout(diagram); |
| levelLayout.adjust = adjust; |
| |
| diagramLayout = levelLayout; |
| } |
| return diagramLayout; |
| } |
| |
| private String config(final String name, final String defaultValue) { |
| final String cst = layout.substring(name.length()); |
| String len = defaultValue; |
| if (!cst.isEmpty()) { |
| len = cst; |
| } |
| return len; |
| } |
| |
| private JFrame createWindow(final VisualizationViewer<Node, Edge> viewer, final String name) { |
| viewer.setBackground(Color.WHITE); |
| |
| final DefaultModalGraphMouse<Node, Edge> gm = new DefaultModalGraphMouse<Node, Edge>(); |
| gm.setMode(DefaultModalGraphMouse.Mode.PICKING); |
| viewer.setGraphMouse(gm); |
| |
| final JFrame frame = new JFrame(name + " viewer"); |
| frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); |
| frame.setLayout(new GridLayout()); |
| frame.getContentPane().add(viewer); |
| frame.pack(); |
| |
| return frame; |
| } |
| |
| private void saveView(final Dimension currentSize, final Dimension desiredSize, final String name, final VisualizationViewer<Node, Edge> viewer) throws MojoExecutionException { |
| BufferedImage bi = new BufferedImage(currentSize.width, currentSize.height, BufferedImage.TYPE_INT_ARGB); |
| |
| final Graphics2D g = bi.createGraphics(); |
| g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
| g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); |
| g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
| |
| final boolean db = viewer.isDoubleBuffered(); |
| viewer.setDoubleBuffered(false); |
| viewer.paint(g); |
| viewer.setDoubleBuffered(db); |
| if (!currentSize.equals(desiredSize)) { |
| final double xFactor = desiredSize.width * 1. / currentSize.width; |
| final double yFactor = desiredSize.height * 1. / currentSize.height; |
| final double factor = Math.min(xFactor, yFactor); |
| getLog().info("optimal size is (" + currentSize.width + ", " + currentSize.height + ")"); |
| getLog().info("scaling with a factor of " + factor); |
| |
| final AffineTransform tx = new AffineTransform(); |
| tx.scale(factor, factor); |
| final AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); |
| BufferedImage biNew = new BufferedImage((int) (bi.getWidth() * factor), (int) (bi.getHeight() * factor), bi.getType()); |
| bi = op.filter(bi, biNew); |
| } |
| g.dispose(); |
| |
| OutputStream os = null; |
| try { |
| final File file = new File(output, (outputFileName != null ? outputFileName : name) + "." + format); |
| os = new FileOutputStream(file); |
| if (!ImageIO.write(bi, format, os)) { |
| throw new MojoExecutionException("can't save picture " + name + "." + format); |
| } |
| getLog().info("Saved " + file.getAbsolutePath()); |
| } catch (final IOException e) { |
| throw new MojoExecutionException("can't save the diagram", e); |
| } finally { |
| if (os != null) { |
| try { |
| os.flush(); |
| os.close(); |
| } catch (final IOException e) { |
| // no-op |
| } |
| } |
| } |
| } |
| |
| |
| private File validInput() throws MojoExecutionException { |
| final File file = new File(path); |
| if (!file.exists()) { |
| final String msg = "Can't find '" + path + "'"; |
| if (failIfMissing) { |
| throw new MojoExecutionException(msg); |
| } |
| getLog().error(msg); |
| } |
| return file; |
| } |
| |
| private String slurp(final File file) throws MojoExecutionException { |
| final String content; |
| FileInputStream fis = null; |
| try { |
| fis = new FileInputStream(file); |
| content = IOUtil.toString(fis); |
| } catch (final Exception e) { |
| throw new MojoExecutionException(e.getMessage(), e); |
| } finally { |
| IOUtil.close(fis); |
| } |
| return content; |
| } |
| |
| private static class Diagram extends DirectedSparseGraph<Node, Edge> { |
| private final String name; |
| |
| private Diagram(final String name) { |
| this.name = name; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| } |
| |
| private static class Node { |
| public static enum Type { |
| STEP, SINK |
| } |
| |
| private final String text; |
| private final Type type; |
| private boolean root = false; |
| |
| private Node(final String text, final Type type) { |
| this.text = text; |
| this.type = type; |
| } |
| |
| public void root() { |
| root = true; |
| } |
| } |
| |
| private static class Edge { |
| private final String text; |
| |
| private Edge(final String text) { |
| this.text = text; |
| } |
| } |
| |
| private static class GraphViewer extends VisualizationViewer<Node, Edge> { |
| private final boolean rotateEdges; |
| |
| public GraphViewer(final Layout<Node, Edge> nodeEdgeLayout, final boolean rotateEdges) { |
| super(nodeEdgeLayout); |
| this.rotateEdges = rotateEdges; |
| init(); |
| } |
| |
| private void init() { |
| setOpaque(true); |
| setBackground(new Color(255, 255, 255, 0)); |
| |
| final RenderContext<Node, Edge> context = getRenderContext(); |
| |
| context.setVertexFillPaintTransformer(new VertexFillPaintTransformer()); |
| context.setVertexShapeTransformer(new VertexShapeTransformer(getFontMetrics(getFont()))); |
| context.setVertexLabelTransformer(new VertexLabelTransformer()); |
| getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR); |
| |
| context.setEdgeLabelTransformer(new EdgeLabelTransformer()); |
| context.setEdgeShapeTransformer(new EdgeShape.Line<Node, Edge>()); |
| context.setEdgeLabelClosenessTransformer(new EdgeLabelClosenessTransformer()); |
| context.getEdgeLabelRenderer().setRotateEdgeLabels(rotateEdges); |
| getRenderer().setEdgeLabelRenderer(new EdgeLabelRenderer()); |
| } |
| } |
| |
| private static class VertexShapeTransformer implements Transformer<Node, Shape> { |
| private static final int X_MARGIN = 4; |
| private static final int Y_MARGIN = 2; |
| |
| private FontMetrics metrics; |
| |
| public VertexShapeTransformer(final FontMetrics f) { |
| metrics = f; |
| } |
| |
| @Override |
| public Shape transform(final Node i) { |
| final int w = metrics.stringWidth(i.text) + X_MARGIN; |
| final int h = metrics.getHeight() + Y_MARGIN; |
| |
| // centering |
| final AffineTransform transform = AffineTransform.getTranslateInstance(-w / 2.0, -h / 2.0); |
| switch (i.type) { |
| case SINK: |
| return transform.createTransformedShape(new Ellipse2D.Double(0, 0, w, h)); |
| default: |
| return transform.createTransformedShape(new Rectangle(0, 0, w, h)); |
| } |
| } |
| } |
| |
| private static class VertexFillPaintTransformer implements Transformer<Node, Paint> { |
| @Override |
| public Paint transform(final Node node) { |
| if (node.root) { |
| return Color.GREEN; |
| } |
| |
| switch (node.type) { |
| case SINK: |
| return Color.RED; |
| default: |
| return Color.WHITE; |
| } |
| } |
| } |
| |
| private static class EdgeLabelTransformer implements Transformer<Edge, String> { |
| @Override |
| public String transform(Edge i) { |
| return i.text; |
| } |
| } |
| |
| private static class VertexLabelTransformer extends ToStringLabeller<Node> { |
| public String transform(final Node node) { |
| return node.text; |
| } |
| } |
| |
| private static class EdgeLabelClosenessTransformer implements Transformer<Context<Graph<Node, Edge>, Edge>, Number> { |
| @Override |
| public Number transform(final Context<Graph<Node, Edge>, Edge> context) { |
| return 0.5; |
| } |
| } |
| |
| private static class EdgeLabelRenderer extends BasicEdgeLabelRenderer<Node, Edge> { |
| public void labelEdge(final RenderContext<Node, Edge> rc, final Layout<Node, Edge> layout, final Edge e, final String label) { |
| if (label == null || label.length() == 0) { |
| return; |
| } |
| |
| final Graph<Node, Edge> graph = layout.getGraph(); |
| // don't draw edge if either incident vertex is not drawn |
| final Pair<Node> endpoints = graph.getEndpoints(e); |
| final Node v1 = endpoints.getFirst(); |
| final Node v2 = endpoints.getSecond(); |
| if (!rc.getEdgeIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Edge>getInstance(graph, e))) { |
| return; |
| } |
| |
| if (!rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v1)) || |
| !rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v2))) { |
| return; |
| } |
| |
| final Point2D p1 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v1)); |
| final Point2D p2 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v2)); |
| |
| final GraphicsDecorator g = rc.getGraphicsContext(); |
| final Component component = prepareRenderer(rc, rc.getEdgeLabelRenderer(), label, rc.getPickedEdgeState().isPicked(e), e); |
| final Dimension d = component.getPreferredSize(); |
| |
| final AffineTransform old = g.getTransform(); |
| final AffineTransform xform = new AffineTransform(old); |
| final FontMetrics fm = g.getFontMetrics(); |
| int w = fm.stringWidth(e.text); |
| double p = Math.max(0, p1.getX() + p2.getX() - w); |
| xform.translate(Math.min(layout.getSize().width - w, p / 2), (p1.getY() + p2.getY() - fm.getHeight()) / 2); |
| g.setTransform(xform); |
| g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true); |
| |
| g.setTransform(old); |
| } |
| } |
| |
| private static class LevelLayout extends AbstractLayout<Node, Edge> { |
| private static final int X_MARGIN = 4; |
| |
| private Transformer<Node, Shape> vertexShapeTransformer = null; |
| private boolean adjust; |
| |
| public LevelLayout(final Diagram nodeEdgeGraph) { |
| super(nodeEdgeGraph); |
| } |
| |
| @Override |
| public void initialize() { |
| final Map<Node, Integer> level = levels(); |
| final List<List<Node>> nodes = sortNodeByLevel(level); |
| final int ySpace = maxHeight(nodes); |
| final int nLevels = nodes.size(); |
| final int yLevel = Math.max(0, getSize().height - nLevels * ySpace) / Math.max(1, nLevels - 1); |
| |
| int y = ySpace / 2; |
| int maxWidth = getSize().width; |
| for (final List<Node> currentNodes : nodes) { |
| if (currentNodes.size() == 1) { // only 1 => centering manually |
| setLocation(currentNodes.iterator().next(), new Point(getSize().width / 2, y)); |
| } else { |
| int x = 0; |
| final int xLevel = Math.max(0, getSize().width - width(currentNodes) - X_MARGIN) / (currentNodes.size() - 1); |
| Collections.sort(currentNodes, new NodeComparator((Diagram) graph, locations)); |
| |
| for (Node node : currentNodes) { |
| Rectangle b = getBound(node, vertexShapeTransformer); |
| int step = b.getBounds().width / 2; |
| x += step; |
| setLocation(node, new Point(x, y)); |
| x += xLevel + step; |
| } |
| |
| maxWidth = Math.max(maxWidth, x - xLevel); |
| } |
| y += yLevel + ySpace; |
| } |
| |
| if (adjust) { |
| adjust = false; |
| setSize(new Dimension(maxWidth, y + ySpace)); |
| initialize(); |
| adjust = true; |
| } |
| } |
| |
| @Override |
| public void reset() { |
| initialize(); |
| } |
| |
| private int width(List<Node> nodes) { |
| int sum = 0; |
| for (Node node : nodes) { |
| sum += getBound(node, vertexShapeTransformer).width; |
| } |
| return sum; |
| } |
| |
| private int maxHeight(final List<List<Node>> nodes) { |
| int max = 0; |
| for (final List<Node> list : nodes) { |
| for (final Node n : list) { |
| max = Math.max(max, getBound(n, vertexShapeTransformer).height); |
| } |
| } |
| return max; |
| } |
| |
| private Rectangle getBound(final Node n, final Transformer<Node, Shape> vst) { |
| if (vst == null) { |
| return new Rectangle(0, 0); |
| } |
| return vst.transform(n).getBounds(); |
| } |
| |
| private List<List<Node>> sortNodeByLevel(final Map<Node, Integer> level) { |
| final int levels = max(level); |
| |
| final List<List<Node>> sorted = new ArrayList<List<Node>>(); |
| for (int i = 0; i < levels; i++) { |
| sorted.add(new ArrayList<Node>()); |
| } |
| |
| for (final Map.Entry<Node, Integer> entry : level.entrySet()) { |
| sorted.get(entry.getValue()).add(entry.getKey()); |
| } |
| return sorted; |
| } |
| |
| private int max(final Map<Node, Integer> level) { |
| int i = 0; |
| for (Map.Entry<Node, Integer> l : level.entrySet()) { |
| if (l.getValue() >= i) { |
| i = l.getValue() + 1; |
| } |
| } |
| return i; |
| } |
| |
| private Map<Node, Integer> levels() { |
| final Map<Node, Integer> out = new HashMap<Node, Integer>(); |
| for (final Node node : graph.getVertices()) { // init |
| out.put(node, 0); |
| } |
| |
| final Map<Node, Collection<Node>> successors = new HashMap<Node, Collection<Node>>(); |
| final Map<Node, Collection<Node>> predecessors = new HashMap<Node, Collection<Node>>(); |
| for (final Node node : graph.getVertices()) { |
| successors.put(node, graph.getSuccessors(node)); |
| predecessors.put(node, graph.getPredecessors(node)); |
| } |
| |
| boolean done; |
| do { |
| done = true; |
| for (final Node node : graph.getVertices()) { |
| int nodeLevel = out.get(node); |
| for (final Node successor : successors.get(node)) { |
| if (out.get(successor) <= nodeLevel |
| && successor != node |
| && !predecessors.get(node).contains(successor)) { |
| done = false; |
| out.put(successor, nodeLevel + 1); |
| } |
| } |
| } |
| } while (!done); |
| |
| final int min = Collections.min(out.values()); |
| for (final Map.Entry<Node, Integer> entry : out.entrySet()) { |
| out.put(entry.getKey(), entry.getValue() - min); |
| } |
| |
| return out; |
| } |
| } |
| |
| private static class NodeComparator implements Comparator<Node> { // sort by predecessor location |
| private final Diagram graph; |
| private final Map<Node, Point2D> locations; |
| |
| public NodeComparator(final Diagram diagram, final Map<Node, Point2D> points) { |
| graph = diagram; |
| locations = points; |
| } |
| |
| @Override |
| public int compare(Node o1, Node o2) { |
| final Collection<Node> p1 = graph.getPredecessors(o1); |
| final Collection<Node> p2 = graph.getPredecessors(o2); |
| |
| // mean value is used but almost always there is only one predecessor |
| final int m1 = mean(p1); |
| final int m2 = mean(p2); |
| return m1 - m2; |
| } |
| |
| private int mean(final Collection<Node> p) { |
| if (p.size() == 0) { |
| return 0; |
| } |
| int mean = 0; |
| for (final Node n : p) { |
| mean += locations.get(n).getX(); |
| } |
| return mean / p.size(); |
| } |
| } |
| } |