es.ucm.fdi.clover.view.Animator.java Source code

Java tutorial

Introduction

Here is the source code for es.ucm.fdi.clover.view.Animator.java

Source

/**
 * AC - A source-code copy detector
 *
 *     For more information please visit:  http://github.com/manuel-freire/ac
 *
 * ****************************************************************************
 *
 * This file is part of AC, version 2.0
 *
 * AC is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * AC 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with AC.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Animator.java
 *
 * Created on May 6, 2006, 11:36 PM
 * Original Author: Manuel Freire (manuel.freire@uam.es)
 */

package es.ucm.fdi.clover.view;

import es.ucm.fdi.clover.event.StructureChangeEvent;
import es.ucm.fdi.clover.event.StructureChangeListener;
import es.ucm.fdi.clover.layout.LayoutCache;
import es.ucm.fdi.clover.layout.LayoutManager;
import es.ucm.fdi.clover.layout.LayoutCritic;
import es.ucm.fdi.clover.layout.Node;
import es.ucm.fdi.clover.layout.ForceTreeLayout;
import es.ucm.fdi.clover.model.BaseGraph;
import es.ucm.fdi.clover.model.Edge;
import es.ucm.fdi.clover.model.ViewGraph;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import org.jgrapht.Graphs;
import org.jgrapht.ext.JGraphModelAdapter;
import org.jgraph.event.GraphModelEvent;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.GraphCell;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;

import es.ucm.fdi.clover.layout.BoundVarLengthFDL;
import es.ucm.fdi.clover.layout.ConstantEdgeLengthFDL;
import es.ucm.fdi.clover.layout.FTALayout;
import es.ucm.fdi.clover.layout.SimpleBoxLayout;
import es.ucm.fdi.clover.layout.VarLengthFDL;
import es.ucm.fdi.clover.layout.VerticalBoxLayout;
import java.awt.geom.Rectangle2D;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jdom2.Element;

/**
 * An Animator is in charge of animating changes from one graph to another. 
 * Ideally, the "from" graph will be similar to the "to" graph, and the changes
 * will be small and easy to evidence.
 *
 * Initial layout is also performed by the animator - but in an instant manner.
 *
 * @author mfreire
 */
public class Animator implements StructureChangeListener {

    static private Logger log = Logger.getLogger(Animator.class);

    /** manager - good to run layouts with */
    protected LayoutManager manager;

    /** view to operate on */
    protected BaseView view;

    /** layout cache (ours), to avoid 
     * recalculating expensive layouts and provide mental map support 
     */
    protected LayoutCache layoutCache;

    /** JGraph's layout cache, only used to update current cell positions */
    protected GraphLayoutCache glc;

    /** animation plan; allows incremental steps to be added after each other */
    protected AnimationPlan plan;

    protected VarLengthFDL vlfdl;
    protected BoundVarLengthFDL bvlfdl;
    protected VerticalBoxLayout vbl;
    protected FTALayout ftal;
    protected SimpleBoxLayout sbl;
    protected ConstantEdgeLengthFDL celfdl;

    // settings that get saved & restored
    protected int maxInterpolationTime = 2000;
    protected int initialLayoutArea = 300;
    protected int initialLayoutTime = maxInterpolationTime * 2;
    protected double cacheSloppynessLimit = 0.9;
    protected int incrementalRefinementPasses = 10;

    /**
     * Creates a new instance of Animator
     */
    public Animator(BaseView view) {
        vlfdl = new VarLengthFDL();
        bvlfdl = new BoundVarLengthFDL();
        bvlfdl.setMaxIterations(100);
        vbl = new VerticalBoxLayout();
        ftal = new FTALayout();
        sbl = new SimpleBoxLayout();
        celfdl = new ConstantEdgeLengthFDL();
        celfdl.setMaxIterations(80);
        celfdl.setIdealDistance(120);

        manager = new LayoutManager(view);
        layoutCache = new LayoutCache();
        setView(view);
    }

    public void setView(BaseView view) {
        if (this.view != null) {
            BaseGraph base = this.view.getBase();
            if (base != view.getBase()) {
                base.removeStructureChangeListener(this);
            }
        }
        this.view = view;
        glc = view.getGraphLayoutCache();
        view.getBase().addStructureChangeListener(this);
        start();
    }

    public LayoutManager getLayoutManager() {
        return manager;
    }

    public LayoutCache getLayoutCache() {
        return layoutCache;
    }

    public void setLayoutCache(LayoutCache layoutCache) {
        this.layoutCache = layoutCache;
    }

    public void start() {
        if (view.getViewGraph().vertexSet().size() == 0)
            return;

        manager.setNodes(view);

        manager.setAlgorithm(new ForceTreeLayout(120));
        manager.run();
        manager.applyChanges(view);
        manager.setAlgorithm(vlfdl);
        manager.setMaxTime(initialLayoutTime);
        manager.run();
        manager.setAlgorithm(celfdl);
        manager.run();
        manager.setAlgorithm(vbl);
        manager.run();
        manager.setAlgorithm(ftal);
        manager.run();
        manager.setAlgorithm(sbl);
        manager.run();
        manager.applyChanges(view);

        manager.addNodesToCache(layoutCache, view);
    }

    protected InterpolatedMovementStep incrementalLayoutStep(HashSet freeViews, int n) {
        return incrementalLayoutStep(freeViews, n, true);
    }

    protected InterpolatedMovementStep incrementalLayoutStep(HashSet freeViews, int n, boolean useOldPositions) {
        return incrementalLayoutStep(freeViews, n, true, cacheSloppynessLimit);
    }

    /**
     * Designed to be called from outside to beautify already layed-out graphs
     * (without destroying them too much)
     */
    public void doIncrementalLayout() {
        AnimationPlan ap = new AnimationPlan(view, AnimationPlan.RELAYOUT_PRIORITY);
        ap.addStep(incrementalLayoutStep(new HashSet(), incrementalRefinementPasses, false, 2));
        log.debug("Starting ANIMATION PLAN to beautify layout");
        ap.run();
    }

    /**
     * Performs incremental layout and configures an "InterpolatedMovementStep"
     * to display it.
     * @param freeViews view cells that should not be constrained to their current
     *      positions
     * @param n number of passes to perform
     * @param useOldPositions if true, the 'current' positions are taken from 
     *      the last layout performed in the manager (which may not be yet
     *      visible on-screen). if false, the 'current' positions will be those
     *      now visible on screen. Use 'true' to chain several layouts.
     * @param minScore minimum score (in range 0-1) for a cache search to be 
     *      considered a hit. Negative values mean "don't use cache".
     * @return an AnimationStep that animates the transition from the old to the
     *      new layout.
     */
    protected InterpolatedMovementStep incrementalLayoutStep(HashSet freeViews, int n, boolean useOldPositions,
            double minScore) {

        manager.setNodes(view, useOldPositions);
        Rectangle2D bounds;
        bounds = Node.getBounds(manager.getNodes(), view.getLayoutZoom());
        Map m1 = Node.getChangeMap(manager.getNodes(), (int) bounds.getX() - 10, (int) bounds.getY() - 10);

        // check cache for a hit
        double cacheQuality = 0;

        if (minScore >= 0) {
            cacheQuality = manager.setNodesFromCache(layoutCache, view, minScore);
            if (cacheQuality > 0 && cacheQuality < 1) {
                log.debug("Using cached layout, old passes = " + n);
                // q=1 => n/=4; q=.63 => n/=1; q<.63, increments
                n = Math.max((int) (n * ((1 - cacheQuality) * 2 + .25)), 2);
                log.debug("\treduced to passes = " + n);
            }
        }

        // if not a perfect hit, some layout is still required
        if (cacheQuality < 1) {
            log.debug("Because cache was not perfect, layout out for n = " + n);

            LayoutCritic q = new LayoutCritic(manager.getNodes());
            q.getQuality();

            manager.setAlgorithm(bvlfdl);
            bvlfdl.setFreeNodes(freeViews, n * 2);
            manager.setMaxTime(maxInterpolationTime);
            manager.run();
            celfdl.setFreeNodes(freeViews, n * 2);
            manager.setAlgorithm(celfdl);
            manager.run();

            if (q.getNodeOverlaps() > 0 || q.getComponentOverlaps() > 0) {
                manager.setAlgorithm(vbl);
                manager.run();
                manager.setAlgorithm(ftal);
                manager.run();
                manager.setAlgorithm(sbl);
                manager.run();
            } else {
                log.debug("Avoided having to fix layout; critic said it was fine");
            }
        } else {
            log.debug("REUSING prior layout");
        }

        bounds = Node.getBounds(manager.getNodes(), view.getLayoutZoom());
        Map m2 = Node.getChangeMap(manager.getNodes(), (int) bounds.getX() - 10, (int) bounds.getY() - 10);

        // store in cache for later use
        manager.addNodesToCache(layoutCache, view);

        return new InterpolatedMovementStep(m1, m2);
    }

    protected HashSet verticesToViews(Collection vertices) {
        HashSet cellViews = new HashSet();
        JGraphModelAdapter adapter = view.getViewGraph().getModelAdapter();
        for (Object o : vertices) {
            GraphCell c = adapter.getVertexCell(o);
            cellViews.add(glc.getMapping(c, true));
        }
        return cellViews;
    }

    /**
     * FIXME: once changes get really structured, this should reflect it.
     */
    public void structureChangePerformed(StructureChangeEvent evt) {
        ViewGraph g = view.getViewGraph();
        HashSet free = new HashSet();

        plan = new AnimationPlan(view, AnimationPlan.STRUCTURE_PRIORITY);

        manager.setNodes(view);

        if (log.isDebugEnabled()) {
            log.debug("Change received: " + evt.getDescription());
        }

        // remove old vertices
        int n = 0;
        for (Object v : evt.getRemovedVertices()) {
            g.removeVertex(v);
            n++;
        }
        if (n > 0) {
            plan.addStep(incrementalLayoutStep(free, n + 2));
        }

        // remove old edges
        free.clear();
        n = 0;
        for (Edge e : evt.getRemovedEdges()) {
            g.removeEdge(e.getSource(), e.getTarget());
            free.add(e.getSource());
            free.add(e.getTarget());
            n++;
        }
        if (n > 0) {
            plan.addStep(incrementalLayoutStep(verticesToViews(free), n + 2));
        }

        // this is good for new-node-pos initialization (see below)
        free.addAll(evt.getAddedVertices());
        manager.setNodes(view, true);
        Map<Object, Rectangle2D> vertexPos = Node.getPositions(manager.getNodes(), view);

        // and new edges        
        n = 0;
        for (Edge e : evt.getAddedEdges()) {
            // initialize free end-vertices positions to that of the edge            
            Object src = e.getSource();
            Object dst = e.getTarget();

            //            System.err.println("Edge is "+e+" from "+src+" to "+dst+"; g is "+g+
            //                    ", and "+vertexPos.get(src)+" and "+vertexPos.get(dst));

            Object v = null;
            Object o = null;
            if (free.contains(src)) {
                v = src;
                o = dst;
            }
            if (free.contains(dst)) {
                v = (v == null) ? dst : v;
                o = src;
            }
            if (v != null) {
                if (vertexPos.get(o) != null) {
                    g.addVertex(v, (Rectangle2D) vertexPos.get(o).clone());
                } else {
                    //g.addVertex(v);                    
                    // if things are missing, that's because this is really a clustering event...
                    log.warn("WARNING: this should not be called (clustering stuff at the base Animator!)");
                    //Thread.dumpStack();
                    //return;
                }
            }
        }

        // add all new vertices (redundant adds dont trigger anything)
        Graphs.addAllVertices(g, evt.getAddedVertices());

        for (Edge e : evt.getAddedEdges()) {
            g.addEdge(e.getSource(), e.getTarget(), e);
            free.add(e.getSource());
            free.add(e.getTarget());
            n++;
        }
        if (n > 0) {
            plan.addStep(incrementalLayoutStep(verticesToViews(free), n + 3));
        }

        // this highlights a changed node, and updates its labels and stuff
        Map changes = new HashMap();
        for (Object v : evt.getChangedVertices()) {
            DefaultGraphCell c = ViewHelper.getVertexCell(view, v);
            if (c == null) {
                log.warn("Can't find cell for " + v + " in the graph!");
                for (Object v2 : view.getViewGraph().vertexSet()) {
                    log.warn(" ... But I found " + v2);
                }
                continue;
            }
            log.debug("Redecorating vertex " + c);
            view.getViewGraph().decorateVertexCell(c);
            changes.put(c, c.getAttributes());
        }
        for (Edge e : evt.getChangedEdges()) {
            DefaultEdge de = ViewHelper.getEdgeCell(view, e);
            view.getViewGraph().decorateEdgeCell(de);
            changes.put(de, de.getAttributes());
        }
        // FIXME : add highlighting
        view.getGraphLayoutCache().edit(changes);

        // run the plan
        if (!plan.getMoves().isEmpty()) {
            log.debug("Starting ANIMATION PLAN to show off " + evt.getDescription());
            plan.run();
        }
    }

    public LayoutManager getManager() {
        return manager;
    }

    /**
     * Resyncs using a GraphModelEvent event; should be much more efficient
     * than the alternative
     */
    public void resyncFromEvent(GraphModelEvent evt) {
        GraphModelEvent.GraphModelChange c = evt.getChange();
        if (c.getRemoved() != null || c.getInserted() != null) {
            if (log.isDebugEnabled()) {
                log.debug("Could not use event " + evt + " incrementally; ignoring");
            }
            return;
        }

        Node[] nodes = manager.getNodes();
        HashMap<Object, Rectangle2D> move = new HashMap<Object, Rectangle2D>();
        for (Object k : c.getPreviousAttributes().keySet()) {
            if (!(k instanceof DefaultGraphCell)) {
                log.warn("Change did not refer to a defaultgraphcell! - was " + k + " (" + k.getClass().getName()
                        + ")");
                continue;
            }
            DefaultGraphCell cell = (DefaultGraphCell) k;
            Rectangle2D bounds = GraphConstants.getBounds((Map) c.getPreviousAttributes().get(k));

            if (cell == null || bounds == null) {
                continue;
            }

            //System.err.println("Changed bounds of "+cell+" to "+bounds);
            move.put(ViewHelper.getVertex(cell), bounds);
        }

        if (!move.isEmpty()) {
            Node.setPositions(move, nodes, view);
            manager.addNodesToCache(layoutCache, view);
        }
    }

    /**
     * Uses current view information to set the current layout; also
     * stores it in the layout cache
     */
    public void resync() {
        manager.setNodes(view);
        manager.addNodesToCache(layoutCache, view);
    }

    /**
     * Used to debug the layout cache in use
     */
    void dumpLayoutCache(BaseGraph g) {
        System.err.println("Layout cache is:\n" + layoutCache.dump(g));
    }

    /**
     * Saves the animator's settings to a string
     */
    public void save(Element e) {
        e.setAttribute("maxInterpolationTime", "" + maxInterpolationTime);
        e.setAttribute("initialLayoutArea", "" + initialLayoutArea);
        e.setAttribute("initialLayoutTime", "" + initialLayoutTime);
        e.setAttribute("cacheSloppynessLimit", "" + cacheSloppynessLimit);
        e.setAttribute("incrementalRefinementPasses", "" + incrementalRefinementPasses);
    }

    /**
     * Restores the animator's settings from a string
     */
    public void restore(Element e) {
        maxInterpolationTime = Integer.parseInt(e.getAttributeValue("maxInterpolationTime"));
        initialLayoutArea = Integer.parseInt(e.getAttributeValue("initialLayoutArea"));
        initialLayoutTime = Integer.parseInt(e.getAttributeValue("initialLayoutTime"));
        cacheSloppynessLimit = Float.parseFloat(e.getAttributeValue("cacheSloppynessLimit"));
        incrementalRefinementPasses = Integer.parseInt(e.getAttributeValue("incrementalRefinementPasses"));
    }
}