SiLeBAT/FSK-Lab

View on GitHub
de.bund.bfr.knime.foodprocess/src/de/bund/bfr/knime/foodprocess/ui/LayoutManager.java

Summary

Maintainability
F
3 days
Test Coverage
package de.bund.bfr.knime.foodprocess.ui;

/*******************************************************************************
 * Copyright (c) 2015 Federal Institute for Risk Assessment (BfR), Germany
 *
 * This program 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.
 *
 * This program is 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 program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *     Department Biological Safety - BfR
 *******************************************************************************/

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.draw2d.geometry.Point;
import org.knime.core.node.NodeLogger;
import org.knime.core.node.workflow.ConnectionContainer;
import org.knime.core.node.workflow.ConnectionID;
import org.knime.core.node.workflow.ConnectionUIInformation;
import org.knime.core.node.workflow.NodeContainer;
import org.knime.core.node.workflow.NodeID;
import org.knime.core.node.workflow.NodeUIInformation;
import org.knime.core.node.workflow.WorkflowManager;
import org.knime.workbench.editor2.WorkflowEditor;
import org.knime.workbench.ui.layout.Graph;
import org.knime.workbench.ui.layout.Graph.Edge;
import org.knime.workbench.ui.layout.Graph.Node;
import org.knime.workbench.ui.layout.layeredlayout.SimpleLayeredLayouter;

/**
 *
 * @author mader, University of Konstanz
 */
public class LayoutManager {

    private static final NodeLogger LOGGER = NodeLogger
            .getLogger(LayoutManager.class);

    private WorkflowManager m_wfm;

    private long m_initPlacementSeed;

    private HashMap<NodeContainer, Node> m_workbenchToGraphNodes;

    private HashMap<ConnectionContainer, Edge> m_workbenchToGraphEdges;

    // nodes not laid out - but connected to nodes being laid out
    private HashMap<NodeContainer, Node> m_workbenchIncomingNodes;

    // nodes not laid out - but connected to nodes being laid out
    private HashMap<NodeContainer, Node> m_workbenchOutgoingNodes;

    // Meta node incoming port indices connected to nodes being laid out
    private HashMap<Integer, Node> m_workbenchWFMInports;

    // Meta node outgoing port indices connected to nodes being laid out
    private HashMap<Integer, Node> m_workbenchWFMOutports;

    /* the graph stores only one edge between two nodes. The connections
     * represented are in the list.
     */
    private HashMap<Edge, List<ConnectionContainer>> m_parallelConns;

    private Graph m_g;

    private HashMap<NodeID, NodeUIInformation> m_oldCoordinates;

    private HashMap<ConnectionID, ConnectionUIInformation> m_oldBendpoints;

    /**
     * The constructor.
     *
     * @param wfManager contains the flow being laid out
     */
    public LayoutManager(final WorkflowManager wfManager,
            final long initialPlacementSeed) {
        m_wfm = wfManager;
        m_initPlacementSeed = initialPlacementSeed;
        m_workbenchToGraphNodes = new HashMap<NodeContainer, Graph.Node>();
        m_workbenchToGraphEdges =
                new HashMap<ConnectionContainer, Graph.Edge>();
        m_workbenchIncomingNodes = new HashMap<NodeContainer, Graph.Node>();
        m_workbenchOutgoingNodes = new HashMap<NodeContainer, Graph.Node>();
        m_workbenchWFMInports = new HashMap<Integer, Graph.Node>();
        m_workbenchWFMOutports = new HashMap<Integer, Graph.Node>();
        m_parallelConns = new HashMap<Edge, List<ConnectionContainer>>();
        m_g = new Graph();
    }

    /**
     * @param nodes the nodes that should be laid out. If null, all nodes of the
     *            workflow manager passed to the constructor are laid out.
     *
     */
    public void doLayout(final Collection<NodeContainer> nodes) {

        int X_STRETCH = 100;
        int Y_STRETCH = 120;

        if (WorkflowEditor.getActiveEditorSnapToGrid()) {
            if (WorkflowEditor.getActiveEditorGridX() >= 70) {
                X_STRETCH = WorkflowEditor.getActiveEditorGridX();
            } else {
                X_STRETCH = WorkflowEditor.getActiveEditorGridXOffset(X_STRETCH);
            }
            Y_STRETCH = WorkflowEditor.getActiveEditorGridYOffset(Y_STRETCH);
        }
        // add all nodes that should be laid out to the graph
        Collection<NodeContainer> allNodes = nodes;
        if (allNodes == null || allNodes.size() <= 1) {
            allNodes = m_wfm.getNodeContainers();
        }
        // keep the left upper corner of the node cluster.
        // Nodes laid out are placed right and below
        int minX = Integer.MAX_VALUE;
        int minY = Integer.MAX_VALUE;
        // add all nodes that are to be laid out
        for (NodeContainer nc : allNodes) {
            Node gNode = createGraphNodeForNC(nc);
            m_workbenchToGraphNodes.put(nc, gNode);
            NodeUIInformation ui = nc.getUIInformation();
            minX = (ui.getBounds()[0] < minX) ? ui.getBounds()[0] : minX;
            minY = (ui.getBounds()[1] < minY) ? ui.getBounds()[1] : minY;
            if (WorkflowEditor.getActiveEditorSnapToGrid()) {
                Point nextGridLocation = WorkflowEditor.getActiveEditorNextGridLocation(new Point(minX, minY));
                minX = nextGridLocation.x;
                minY = nextGridLocation.y;
            }
        }

        // find all connections that connect from/to our nodes,
        // keep a flag that states: isClusterInternal
        HashMap<ConnectionContainer, Boolean> allConns =
                new HashMap<ConnectionContainer, Boolean>();
        for (ConnectionContainer conn : m_wfm.getConnectionContainers()) {
            Node src = null;
            if (!conn.getSource().equals(m_wfm.getID())) {
                // if it's not a meta node incoming connection
                src =
                        m_workbenchToGraphNodes.get(m_wfm.getNodeContainer(conn
                                .getSource()));
            }
            Node dest = null;
            if (!conn.getDest().equals(m_wfm.getID())) {
                // if it is not a meta node outgoing connection
                dest =
                        m_workbenchToGraphNodes.get(m_wfm.getNodeContainer(conn
                                .getDest()));

            }
            boolean isInternal = (src != null && dest != null);
            // if at least one node is auto laid out we need the connection
            if (src != null || dest != null) {
                allConns.put(conn, isInternal);
            }
        }

        // Add all connections (internal and leading in/out the cluster)
        // to the graph
        Edge gEdge;
        for (ConnectionContainer conn : allConns.keySet()) {
            Node srcGraphNode;
            Node destGraphNode;
            if (conn.getSource().equals(m_wfm.getID())) {
                // it connects to a meta node input port:
                int portIdx = conn.getSourcePort();
                srcGraphNode = m_workbenchWFMInports.get(portIdx);
                if (srcGraphNode == null) {
                    srcGraphNode =
                            m_g.createNode("Incoming " + portIdx, 0, portIdx
                                    * Y_STRETCH);
                    m_workbenchWFMInports.put(portIdx, srcGraphNode);
                }
            } else {
                NodeContainer s = m_wfm.getNodeContainer(conn.getSource());
                srcGraphNode = m_workbenchToGraphNodes.get(s);
                if (srcGraphNode == null) {
                    // then it connects to an "outside" node
                    srcGraphNode = m_workbenchIncomingNodes.get(s);
                    if (srcGraphNode == null) {
                        srcGraphNode = createGraphNodeForNC(s);
                        m_workbenchIncomingNodes.put(s, srcGraphNode);
                    }
                } // else it is a connection inside the layout cluster
            }
            if (conn.getDest().equals(m_wfm.getID())) {
                // it connects to a meta node output port
                int portIdx = conn.getDestPort();
                destGraphNode = m_workbenchWFMOutports.get(portIdx);
                if (destGraphNode == null) {
                    destGraphNode =
                            m_g.createNode("Outgoing " + portIdx, 250, portIdx
                                    * Y_STRETCH);
                    m_workbenchWFMOutports.put(portIdx, destGraphNode);
                }
            } else {
                NodeContainer d = m_wfm.getNodeContainer(conn.getDest());
                destGraphNode = m_workbenchToGraphNodes.get(d);
                if (destGraphNode == null) {
                    // then it connects to an "outside" node
                    destGraphNode = m_workbenchOutgoingNodes.get(d);
                    if (destGraphNode == null) {
                        destGraphNode = createGraphNodeForNC(d);
                        m_workbenchOutgoingNodes.put(d, destGraphNode);
                    }
                } // else it is a connection within the layout cluster
            }

            gEdge = m_g.createEdge(srcGraphNode, destGraphNode);
            if (gEdge != null) {
                m_workbenchToGraphEdges.put(conn, gEdge);
                m_parallelConns.put(gEdge, new LinkedList<ConnectionContainer>(
                        Collections.singletonList(conn)));
            } else {
                // a connection between these node already exists in the graph
                Edge graphEdge = srcGraphNode.getEdge(destGraphNode);
                assert graphEdge != null;
                // add the connection to list of parallel connections.
                m_parallelConns.get(graphEdge).add(conn);
            }
        }

        // AFTER creating all nodes, mark the incoming/outgoing nodes as fixed
        boolean anchorsExist = false;
        Map<Node, Boolean> anchorNodes = m_g.createBoolNodeMap();
        for (Node n : m_workbenchIncomingNodes.values()) {
            anchorsExist = true;
            anchorNodes.put(n, Boolean.TRUE);
        }
        for (Node n : m_workbenchOutgoingNodes.values()) {
            anchorsExist = true;
            anchorNodes.put(n, Boolean.TRUE);
        }
        for (Node n : m_workbenchWFMInports.values()) {
            anchorsExist = true;
            anchorNodes.put(n, Boolean.TRUE);
        }
        for (Node n : m_workbenchWFMOutports.values()) {
            anchorsExist = true;
            anchorNodes.put(n, Boolean.TRUE);
        }

        SimpleLayeredLayouter layouter = new SimpleLayeredLayouter(m_initPlacementSeed);
        layouter.setBalanceBranchings(!WorkflowEditor.getActiveEditorSnapToGrid());
        if (anchorsExist) {
            layouter.doLayout(m_g, anchorNodes);
        } else {
            layouter.doLayout(m_g, null);
        }

        // preserver the old stuff for undoers
        m_oldBendpoints = new HashMap<ConnectionID, ConnectionUIInformation>();
        m_oldCoordinates = new HashMap<NodeID, NodeUIInformation>();

        // transfer new coordinates back to nodes

        // with fixed nodes (lots of) the new coordinates of the nodes may not
        // start at 0.
        double coordOffsetX = Integer.MAX_VALUE;
        double coordOffsetY = Integer.MAX_VALUE;
        for (NodeContainer nc : allNodes) {
            Node gNode = m_workbenchToGraphNodes.get(nc);
            coordOffsetX = Math.min(coordOffsetX, m_g.getX(gNode));
            coordOffsetY = Math.min(coordOffsetY, m_g.getY(gNode));
        }

        for (NodeContainer nc : allNodes) {

            NodeUIInformation uiInfo = nc.getUIInformation();
            if (uiInfo != null) {
                Node gNode = m_workbenchToGraphNodes.get(nc);
                int xval = (int)Math.round(m_g.getX(gNode) - coordOffsetX);
                int[] b = uiInfo.getBounds();
                int x = (int)Math.round(xval * X_STRETCH) + minX;
                int y = (int)Math.round((m_g.getY(gNode) - coordOffsetY)
                        * Y_STRETCH) + minY + (xval % 2 == 0 ? Y_STRETCH/4 : -Y_STRETCH/4);
                NodeUIInformation newCoord = NodeUIInformation.builder()
                        .setNodeLocation(x, y, b[2], b[3])
                        .setHasAbsoluteCoordinates(uiInfo.hasAbsoluteCoordinates())
                        .setSnapToGrid(WorkflowEditor.getActiveEditorSnapToGrid())
                        .build();
                LOGGER.debug("Node " + nc + " gets auto-layout coordinates "
                        + newCoord);
                // save old coordinates for undo
                m_oldCoordinates.put(nc.getID(), uiInfo);
                // triggers gui update
                nc.setUIInformation(newCoord);
            }
        }

        // delete old bendpoints - transfer new ones
        for (ConnectionContainer conn : allConns.keySet()) {

            // store old bendpoint for undo
            ConnectionUIInformation ui = conn.getUIInfo();
            if (ui != null) {
                m_oldBendpoints.put(conn.getID(), ui);
            } else {
                m_oldBendpoints.put(conn.getID(), null);
            }

            ConnectionUIInformation.Builder newUIBuilder = ConnectionUIInformation.builder();
            Edge e = m_workbenchToGraphEdges.get(conn);
            if (e == null) {
                // a parallel connection not represented by the edge
                continue;
            }

            List<ConnectionContainer> conns = m_parallelConns.get(e);
            assert conns.size() > 0;
            assert conns.get(0) == conn; // that is how we created it!

            ArrayList<Point2D> newBends = m_g.bends(e);
            if (newBends != null && !newBends.isEmpty()) {
                int extraX = 16; // half the node icon size...
                int extraY = 24;
                for (int i = 0; i < newBends.size(); i++) {
                    Point2D b = newBends.get(i);
                    newUIBuilder.addBendpoint((int)Math.round((b.getX() - coordOffsetX) * X_STRETCH) + extraX + minX,
                            (int)Math.round((b.getY() - coordOffsetY) * Y_STRETCH) + extraY + minY, i);
                }
            }
            ConnectionUIInformation newUI = newUIBuilder.build();
            conn.setUIInfo(newUI);

            // compute bendpoints for parallel connections (slightly offset)
            for (int i = 1; i < conns.size(); i++) { // idx 0 == conn!
                ConnectionContainer parConn = conns.get(i);
                // destination port determines offset
                int yOffset = (parConn.getDestPort() - conn.getDestPort()) * 10;
                ConnectionUIInformation parUI =
                    createNewWithOffsetPosition(newUI, new int[] {0, yOffset});
                parConn.setUIInfo(parUI);
            }
        }

    }

    /**
     * Creates a new graph node with the coordinates from the UI info and the
     * label set to custom name.
     *
     * @param nc
     * @return
     */
    private Node createGraphNodeForNC(final NodeContainer nc) {
        NodeUIInformation uiInfo = nc.getUIInformation();
        int x = 0;
        int y = 0;
        String label = nc.getCustomName();
        if (label == null || label.isEmpty()) {
            label = "Node " + nc.getID().toString();
        }
        if (uiInfo != null) {
            int[] bounds = uiInfo.getBounds();
            x = bounds[0];
            y = bounds[1];
            return m_g.createNode(label, x, y);
        } else {
            return m_g.createNode(label);
        }

    }

    public Map<NodeID, NodeUIInformation> getOldNodeCoordinates() {
        return Collections.unmodifiableMap(m_oldCoordinates);
    }

    public Map<ConnectionID, ConnectionUIInformation> getOldBendpoints() {
        return Collections.unmodifiableMap(m_oldBendpoints);
    }
    
    private ConnectionUIInformation createNewWithOffsetPosition(
            final ConnectionUIInformation uiInformation,
            final int[] moveDist) {
        
        ConnectionUIInformation.Builder builder = ConnectionUIInformation.builder();
        
        final int[][] originalBendPoints = uiInformation.getAllBendpoints();
        for (int index = 0; index < originalBendPoints.length; index++) {
            int[] point = originalBendPoints[index];
            builder.addBendpoint(point[0] + moveDist[0], point[1] + moveDist[1], index);
        }
        
        return builder.build();
    }
}