com.architexa.diagrams.relo.graph.GraphLayoutManager.java Source code

Java tutorial

Introduction

Here is the source code for com.architexa.diagrams.relo.graph.GraphLayoutManager.java

Source

/* 
 * Copyright (c) 2004-2005 Massachusetts Institute of Technology. This code was
 * developed as part of the Haystack (http://haystack.lcs.mit.edu/) research 
 * project at MIT. Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files (the 
 * "Software"), to deal in the Software without restriction, including without 
 * limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions: 
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software. 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE. 
 */
package com.architexa.diagrams.relo.graph;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jiggle.Cell;
import jiggle.Graph;
import jiggle.Vertex;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.PredicateUtils;
import org.apache.log4j.Logger;
import org.eclipse.swt.widgets.Control;

import com.architexa.diagrams.relo.ReloPlugin;
import com.architexa.diagrams.relo.agent.AgentManager;
import com.architexa.diagrams.relo.agent.ReloBrowseModel;
import com.architexa.diagrams.relo.commands.ServiceCommand;
import com.architexa.diagrams.relo.parts.MoreItemsEditPart;
import com.architexa.diagrams.relo.utils.SVGExport;
import com.architexa.org.eclipse.draw2d.AbstractLayout;
import com.architexa.org.eclipse.draw2d.FlowLayout;
import com.architexa.org.eclipse.draw2d.GraphAnimation;
import com.architexa.org.eclipse.draw2d.IFigure;
import com.architexa.org.eclipse.draw2d.LayoutManager;
import com.architexa.org.eclipse.draw2d.ToolbarLayout;
import com.architexa.org.eclipse.draw2d.geometry.Dimension;
import com.architexa.org.eclipse.draw2d.geometry.Insets;
import com.architexa.org.eclipse.draw2d.geometry.Point;
import com.architexa.org.eclipse.draw2d.geometry.Rectangle;
import com.architexa.org.eclipse.gef.EditPart;
import com.architexa.org.eclipse.gef.RootEditPart;
import com.architexa.org.eclipse.gef.commands.Command;
import com.architexa.org.eclipse.gef.editparts.AbstractConnectionEditPart;
import com.architexa.org.eclipse.gef.editparts.AbstractGraphicalEditPart;

public class GraphLayoutManager extends AbstractLayout {

    private static final Logger progressLogger = ReloPlugin
            .getLogger(GraphLayoutManager.class.getName() + ".Progress");
    private static final Logger logger = ReloPlugin.getLogger(GraphLayoutManager.class);

    public static final int prefEdgeLength = 35;
    public static final int minVertexEdgeDist = 5;
    public static final int prefVertexDist = (int) (prefEdgeLength * 1.25);

    /**
    * The dummy layout class does nothing during normal layouts.  The Graph layout is
    * entirely performed in one place: {@link GraphLayoutManager}, on the diagram's figure.
    * During animation, THIS layout will playback the intermediate steps between the two
    * invocations of the graph layout.
    * @author hudsonr
    */
    public static class SubgraphLayout extends AbstractLayout {
        /**
         * @see com.architexa.diagrams.relo.eclipse.gef.AbstractLayout#calculatePreferredSize(com.architexa.diagrams.relo.eclipse.gef.IFigure, int, int)
         */
        @Override
        protected Dimension calculatePreferredSize(IFigure container, int wHint, int hHint) {
            return null;
        }

        /**
         * @see com.architexa.diagrams.relo.eclipse.gef.LayoutManager#layout(com.architexa.diagrams.relo.eclipse.gef.IFigure)
         */
        public void layout(IFigure container) {
            //   GraphAnimation.recordInitialState(container);
            GraphAnimation.playbackState(container);
        }

    }

    // note: even when disabling the layout manager, we do go through the whole
    //        process as usual, since the subgraphs then get resized 
    //        automatically
    private AgentManager.ViewAgent layoutManagerAgent = new AgentManager.ViewAgent() {
        @Override
        public void setEnabled(boolean enabled) {
            super.setEnabled(enabled);
            if (enabled) {
                // force a relayout
                GraphLayoutManager.this.ageMgr.clearOldParts();
                IFigure f = ((AbstractGraphicalEditPart) GraphLayoutManager.this.rootEditPart.getContents())
                        .getFigure();
                f.getLayoutManager().layout(f);
            }
        }
    };
    {
        ReloBrowseModel.connectToAllModels(layoutManagerAgent);
    }

    public RootEditPart rootEditPart;
    private GraphLayoutDiagram layoutDiag;
    private List<?> selectionTracker;

    public static final Insets PADDING = new Insets(8, 6, 8, 6);

    IGraphLayoutAgeMgr ageMgr = new GraphLayoutAgeMgr();

    // TODO: stubs into ageMgr - move methods or phase out?
    public void anchorPart(AbstractGraphicalEditPart agep, Point newLoc, Point oldLoc) {
        ageMgr.anchorPart(agep, newLoc, oldLoc);
    }

    public void anchorPart(AbstractGraphicalEditPart agep) {
        ageMgr.anchorPart(agep);
    }

    public boolean isPartAnchored(AbstractGraphicalEditPart agep) {
        return ageMgr.isPartAnchored(agep);
    }

    public boolean isLayedOut(AbstractGraphicalEditPart agep) {
        return ageMgr.isLayedOut(agep);
    }

    public void setLayedOut(AbstractGraphicalEditPart agep) {
        ageMgr.setLayedOut(agep);
    }

    public GraphLayoutManager(GraphLayoutDiagram diagram, RootEditPart rootEditPart, List<?> selectionTracker) {
        this.layoutDiag = diagram;
        this.selectionTracker = selectionTracker;
        this.rootEditPart = rootEditPart;

        //jiggle.launchJiggle();

        // Do we want the below (relayout) working again?
        //  The code has the following problems:
        //  o Contribution needs to be added properly (to work for both editor and view)
        //  o Need to get relayout actually working (doesn't in new engine)
        /*
          EditPartViewer viewer = ((ReloController)diagram).getViewer();
          IEditorPart reloEditor = ((DefaultEditDomain) viewer.getEditDomain()).getEditorPart();
          Action layoutUpdaterAction = new Action("Update") {
        @Override
        public void run() {
            Object selectedEP = null;
            Cell selectedEPCell = null;
            for (Object selectedEPObj : GraphLayoutManager.this.selectionTracker) {
                selectedEP = selectedEPObj;
                selectedEPCell = (Cell) oldPartsToCellMap.get(selectedEP);
            }
            // make sure we have a focus
            oldPartsToCellMap.clear();
            if (selectedEPCell != null)
                oldPartsToCellMap.put((AbstractGraphicalEditPart) selectedEP, selectedEPCell);
            
            undoableLayout();
        }
          };
          layoutUpdaterAction.setToolTipText("Update Layout");
          //trackerAction.setText("Layout");
          URL url = ReloPlugin.getDefault().getBundle().getEntry("icons/view-refresh.png");
          layoutUpdaterAction.setImageDescriptor(ImageDescriptor.createFromURL(url));
          IToolBarManager itbm = ((ReloEditorContributor)reloEditor.getEditorSite().getActionBarContributor()).getActionBars().getToolBarManager();
          itbm.add(layoutUpdaterAction);
          */
    }

    @Override
    protected Dimension calculatePreferredSize(IFigure container, int wHint, int hHint) {
        // if (state == PLAYBACK)
        //      return container.getSize();
        container.validate();
        List<?> children = container.getChildren();
        Rectangle result = new Rectangle().setLocation(container.getClientArea().getLocation());

        for (int i = 0; i < children.size(); i++)
            result.union(((IFigure) children.get(i)).getBounds());

        result.resize(container.getInsets().getWidth(), container.getInsets().getHeight());

        return result.getSize();
    }

    void consoleLog(String str) {
        System.out.println(new Date().toString() + ":[" + this.getClass() + "] " + str);
    }

    public void layout(IFigure container) {
        GraphAnimation.recordInitialState(container);
        if (GraphAnimation.playbackState(container))
            return;

        try {
            doRefresh();
        } catch (Exception e) {
            logger.error("Unexpected exception", e);
        }
    }

    private void doRefresh() {
        /* some properties: 
         * 1> we don't want this on the the command stack
         * 2> we are not really doing a layout, but containers might get resized
         * 3> we might want to (really) assert rules here
         * 
         * ideally want to eliminate doing anything here, but otherwise the top-level container has sizing issues  
         */
        //progressLogger.debug("Refresh - Starting!");

        Graph graph = new Graph();

        Map<AbstractGraphicalEditPart, Object> partsToCellMap = new HashMap<AbstractGraphicalEditPart, Object>();
        layoutDiag.contributeNodesToGraph(graph, null, partsToCellMap);

        layoutDiag.contributeEdgesToGraph(graph, partsToCellMap);

        // by default all nodes are on top of each other, causing large forces
        // adjustNodeCoords(graph, partsToCellMap, oldPartsToCellMap);

        //GraphLayoutRules.assertRules(graph, partsToCellMap, anchoredParts);

        // [unfix if a cell has been forced to move ==> removed from refresh, esp. since we don't olden]

        //progressLogger.debug("Refresh Processing - Done! ");

        layoutDiag.applyGraphResults(graph, partsToCellMap);
        //progressLogger.debug("Refresh Manager - Layout Done! ");
    }

    /*
     * There are currently two types of Layout Commands:
     *  1> Instantiated: Done when the method is called (with commands providing undo and redo capabilities)
     *  2> Non-instantiated: This approach does the capture of the UI (to perform layout) when the command
     *     is executed (as opposed to when the command is created / method is called). This is more intuitive
     *     since the execute of another method that this method is chained to might want to do some layout,
     *     and therefore this is the default.
     *     
     *  TODO: see why instantiate layout even exists.
     */

    public Command getLayoutCmd() {
        return getNonInstantiatedLayoutCmd();
    }

    public Command getInstantiatedLayoutCmd() {
        try {
            return getIncrementalLayoutCmd();
        } catch (Exception e) {
            logger.error("Unexpected exception", e);
            return null;
        }
    }

    public Command getNonInstantiatedLayoutCmd() {
        try {
            return new ServiceCommand("Just-in-time Layout") {
                Command instantiatedLayoutCmd = null;

                @Override
                public void execute() {
                    instantiatedLayoutCmd = null;
                    if (instantiatedLayoutCmd == null)
                        instantiatedLayoutCmd = getIncrementalLayoutCmd();
                    if (instantiatedLayoutCmd != null)
                        instantiatedLayoutCmd.execute();
                }

                @Override
                public void undo() {
                    if (instantiatedLayoutCmd != null)
                        instantiatedLayoutCmd.undo();
                }
            };
            //return incrementalLayout();
        } catch (Exception e) {
            logger.error("Unexpected exception", e);
            return null;
        }
    }

    public void undoableLayout() {
        // XXX verify: we don't need to do this (supposedly) because this is being called from a command, and that will do the layout anyway
        if (true)
            return;
        Command cmd = getInstantiatedLayoutCmd();
        //System.err.println("performing undoable layout");
        if (cmd != null)
            rootEditPart.getViewer().getEditDomain().getCommandStack().execute(cmd);
    }

    private Command getIncrementalLayoutCmd() {
        // copy the original graph (needed for undo)
        final Graph origGraph = new Graph();
        final Map<AbstractGraphicalEditPart, Object> origPartsToCellMap = new HashMap<AbstractGraphicalEditPart, Object>();
        layoutDiag.contributeNodesToGraph(origGraph, null, origPartsToCellMap);
        layoutDiag.contributeEdgesToGraph(origGraph, origPartsToCellMap);

        progressLogger.debug("Layout - Starting!");

        Graph graph = new Graph();

        Map<AbstractGraphicalEditPart, Object> partsToCellMap = new HashMap<AbstractGraphicalEditPart, Object>();
        layoutDiag.contributeNodesToGraph(graph, null, partsToCellMap);
        //printNodes(graph);

        // to keep positions
        AbstractGraphicalEditPart mainEP = getFocusedEP(partsToCellMap);
        Point mainEPPos = null;
        //AbstractGraphicalEditPart mainEP = null;
        if (mainEP != null) {
            ageMgr.makeOld(mainEP);
            mainEPPos = mainEP.getFigure().getBounds().getTopLeft();
            //mainEP.getFigure().translateToAbsolute(mainEPPos);
            //System.err.print("Storing position: " + mainEPPos + " - " + mainEPPos2 + " ");
            progressLogger.info("pos1 " + mainEPPos + " mainEP " + mainEP + " :: " + selectionTracker.size());
        }

        layoutDiag.contributeEdgesToGraph(graph, partsToCellMap);

        // grab old cells first (adjusting node coords 'oldens' all cells)

        // by default all nodes are on top of each other, causing large forces
        Point defaultPos = null;
        if (mainEP != null)
            defaultPos = mainEP.getFigure().getBounds().getCenter();
        if (defaultPos == null)
            defaultPos = getCenter();
        adjustNodeCoords(graph, partsToCellMap, defaultPos);

        GraphLayoutRules.assertRules(graph, partsToCellMap, ageMgr.getOldParts());

        if (mainEP != null) {
            // TODO: why are we getting the position a second time (is it because of the adjusting?
            mainEPPos = ((Cell) partsToCellMap.get(mainEP)).getBounds().getTopLeft();
            progressLogger.info("pos2 " + mainEPPos + " mainEP " + mainEP + " :: " + selectionTracker.size());
        }

        //System.err.println("POST-VISIT-DUMP");
        //printNodes(graph);
        //System.err.println("");
        //printEdges(graph);
        //exportGraph(graph, myDesktop, 0);
        //log("visit graph <<<");

        GraphLayoutRules.assertRules(graph, partsToCellMap, ageMgr.getOldParts());

        //exportDiagram(layoutDiag);
        //consoleLog("layout <<<");

        progressLogger.debug("Layout Processing - Done! ");

        // create command

        //final Map<AbstractGraphicalEditPart, Object> newPartsToCellMap = new HashMap<AbstractGraphicalEditPart, Object> (partsToCellMap);
        ///*
        // do a deep copy of partsToCellMap
        final Map<AbstractGraphicalEditPart, Object> newPartsToCellMap = new HashMap<AbstractGraphicalEditPart, Object>(
                partsToCellMap.size());
        for (Map.Entry<AbstractGraphicalEditPart, Object> partsToCellEntry : partsToCellMap.entrySet()) {
            if (partsToCellEntry.getValue() instanceof Vertex)
                newPartsToCellMap.put(partsToCellEntry.getKey(), ((Vertex) partsToCellEntry.getValue()).clone());
            else
                newPartsToCellMap.put(partsToCellEntry.getKey(), partsToCellEntry.getValue());
        }
        //*/

        final Graph newGraph = graph;
        //printChanges(2, newGraph.vertices, origPartsToCellMap, newPartsToCellMap);
        //printChanges(2, newGraph.vertices, oldPartsToCellMap, origPartsToCellMap);
        //printChanges(3, newGraph.vertices, origPartsToCellMap, newPartsToCellMap);

        ageMgr.updateAges(partsToCellMap.keySet());

        return new Command("Instantiated Layout") {
            @Override
            public void execute() {
                layoutDiag.applyGraphResults(newGraph, newPartsToCellMap);
            }

            @Override
            public void undo() {
                layoutDiag.applyGraphResults(origGraph, origPartsToCellMap);
            }
        };
    }

    private org.eclipse.swt.graphics.Point getWindowSize() {
        Control rootCtrl = rootEditPart.getViewer().getControl();
        org.eclipse.swt.graphics.Point rootCtrlSize = rootCtrl.getSize();
        while (rootCtrlSize.x == 0 || rootCtrlSize.y == 0) {
            rootCtrl = rootCtrl.getParent();
            rootCtrlSize = rootCtrl.getSize();
        }
        return rootCtrlSize;
    }

    private Point getCenter() {
        org.eclipse.swt.graphics.Point wndSize = getWindowSize();
        return new Point(wndSize.x / 2, wndSize.y / 2);
    }

    @SuppressWarnings("unchecked")
    private AbstractGraphicalEditPart getFocusedEP(Map<AbstractGraphicalEditPart, Object> partsToCellMap) {
        for (Object selectedObj : new ArrayList(selectionTracker)) {
            EditPart selectedEP = (EditPart) selectedObj;
            while (selectedEP != null && !partsToCellMap.containsKey(selectedEP)) {
                // go up parent tree to node that is layed out by the layout manager
                selectedEP = selectedEP.getParent();
            }
            if (selectedEP != null) {
                // remove stale data
                if (!partsToCellMap.containsKey(selectedEP)) {
                    selectionTracker.remove(selectedEP);
                    continue;
                }
                // make sure that the selected item is not an items that has just been created
                if (ageMgr.isOldPart(selectedEP)) {
                    return (AbstractGraphicalEditPart) selectedEP;
                }
            }
        }
        return null;
    }

    private static int getGraphID(Object obj) {
        Cell cell = (Cell) obj;
        return cell.getGraph().id;
    }

    @SuppressWarnings("unused")
    private static void printChanges(int hdr, Cell[] vertices,
            Map<AbstractGraphicalEditPart, Object> begPartsToCellMap,
            Map<AbstractGraphicalEditPart, Object> endPartsToCellMap) {
        for (Cell cell : vertices) {
            if (!(cell instanceof Vertex))
                continue;
            if (cell.data == null)
                continue;
            MoreItemsEditPart aep = (MoreItemsEditPart) cell.data;

            if (begPartsToCellMap == null || !begPartsToCellMap.containsKey(aep))
                continue;

            Rectangle begBounds = ((Cell) begPartsToCellMap.get(aep)).getBounds();

            Rectangle endBounds = ((Cell) endPartsToCellMap.get(aep)).getBounds();

            printMove(hdr, aep, getGraphID(begPartsToCellMap.get(aep)), begBounds,
                    getGraphID(endPartsToCellMap.get(aep)), endBounds);
        }
    }

    private static void printMove(int hdr, MoreItemsEditPart aep, int begGrID, Rectangle begBounds, int endGrID,
            Rectangle endBounds) {
        Point begTL = begBounds.getTopLeft();
        Point endTL = endBounds.getTopLeft();
        System.err.print(hdr + ": ");
        System.err.print(aep.getLabel(aep.getArtifact().getArt(), null) + ": ");
        if (endTL.getDifference(begTL).getExpanded(1, 1).getArea() > 1) {
            System.err.print(begBounds.getTopLeft() + " --> " + endBounds.getTopLeft());
        } else {
            System.err.print("                       " + endBounds.getTopLeft());
        }
        System.err.println("  move: " + endTL.getDifference(begTL) + ": " + begGrID + "..." + endGrID);
        //(new Exception()).printStackTrace();
    }

    /*
    * By default all nodes are on top of each other, causing large forces
     * Move new nodes 
     */
    private void adjustNodeCoords(Graph graph, Map<AbstractGraphicalEditPart, Object> partsToCellMap,
            Point defaultPos) {
        Set<AbstractGraphicalEditPart> newParts = new HashSet<AbstractGraphicalEditPart>();
        newParts.addAll(partsToCellMap.keySet());
        newParts.removeAll(ageMgr.getOldParts());
        Set<AbstractGraphicalEditPart> newEdges = new HashSet<AbstractGraphicalEditPart>(newParts);
        CollectionUtils.filter(newParts,
                PredicateUtils.notPredicate(PredicateUtils.instanceofPredicate(AbstractConnectionEditPart.class)));
        newEdges.removeAll(newParts);

        for (AbstractGraphicalEditPart edge : new HashSet<AbstractGraphicalEditPart>(newEdges)) {
            if (nodeConstrainedConn((AbstractConnectionEditPart) edge))
                newEdges.remove(edge);
        }

        //System.err.println("adjusting graph: " + newNodes.size() + " done: " + oldParts.size());
        GraphLayoutRules.assertRulesForNewParts(graph, newParts, newEdges, partsToCellMap, ageMgr, defaultPos, this,
                rootEditPart);

        // save for next time
        //fixedLocationParts.addAll(newNodes);
    }

    private boolean nodeConstrainedConn(AbstractConnectionEditPart conn) {
        AbstractGraphicalEditPart srcAGEP = (AbstractGraphicalEditPart) conn.getSource();
        EditPart rootEP = srcAGEP.getRoot().getContents();
        while (srcAGEP != null && srcAGEP != rootEP) {
            LayoutManager lm = srcAGEP.getContentPane().getLayoutManager();
            if (lm instanceof ToolbarLayout || lm instanceof FlowLayout) {
                if (nodeConstrainedConn(srcAGEP, conn))
                    return true;
            }
            srcAGEP = (AbstractGraphicalEditPart) srcAGEP.getParent();
        }
        return false;
    }

    private boolean nodeConstrainedConn(AbstractGraphicalEditPart srcAGEP, AbstractConnectionEditPart conn) {
        AbstractGraphicalEditPart tgtAGEP = (AbstractGraphicalEditPart) conn.getTarget();
        EditPart rootEP = srcAGEP.getRoot().getContents();
        while (tgtAGEP != null && srcAGEP != rootEP) {
            if (tgtAGEP == srcAGEP)
                return true;
            tgtAGEP = (AbstractGraphicalEditPart) tgtAGEP.getParent();
        }
        return false;
    }

    @SuppressWarnings("unused")
    private void exportDiagram(GraphLayoutDiagram diagram) {
        String fileName = System.getProperty("user.home") + "/Desktop/sgef/export.svg";

        SVGExport svgFile = null;
        try {
            svgFile = new SVGExport(fileName);
        } catch (FileNotFoundException e) {
            logger.error("Unexpected exception", e);
            return;
        }

        svgFile.dumpHeader();

        IFigure fig = diagram.getDiagram().getFigure();
        svgFile.exportFigure(fig);

        svgFile.dumpFooter();
    }

    @SuppressWarnings("unused")
    private void exportGraph(Graph graph, String fileBase, int ndx) {
        SVGExport svgFile = null;
        try {
            svgFile = new SVGExport(fileBase + ndx + ".svg");
        } catch (FileNotFoundException e) {
            logger.error("Unexpected exception", e);
            return;
        }
        svgFile.dumpHeader();

        for (int i = 0; i < graph.getNumberOfVertices(); i++) {
            Cell n = graph.vertices.get(i);
            svgFile.exportNode(n, "");
        }

        for (jiggle.Edge edge : graph.edges) {
            svgFile.dumpLine(edge.getFrom(), edge.getTo());
        }

        svgFile.dumpFooter();
    }

    @SuppressWarnings("unused")
    private void printNodes(Graph graph) {
        System.out.println("Nodes");
        for (int i = 0; i < graph.getNumberOfVertices(); i++) {
            Cell n = graph.vertices.get(i);
            System.out.println("[" + i + " - " + n.getMin()[0] + "," + n.getMin()[1] + "x" + n.getSize()[0] + ","
                    + n.getSize()[1] + "]: " + n + ":" + n.getClass() + "/" + n.data + ":" + n.data.getClass());
        }
    }

}