com.offbynull.peernetic.debug.visualizer.JGraphXVisualizer.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.peernetic.debug.visualizer.JGraphXVisualizer.java

Source

/*
 * Copyright (c) 2013-2014, Kasra Faghihi, All rights reserved.
 * 
 * This library 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.0 of the License, or (at your option) any later version.
 * 
 * This library 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 this library.
 */
package com.offbynull.peernetic.debug.visualizer;

import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.MultiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.map.MultiValueMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.ImmutablePair;

/**
 * A {@link Visualizer} implementation that's backed by JGraphX.
 * @author Kasra Faghihi
 * @param <A> address type
 */
public final class JGraphXVisualizer<A> implements Visualizer<A> {

    private JFrame frame;

    private mxGraph graph;
    private mxGraphComponent component;
    private JTextArea textOutputArea;
    private BidiMap<A, Object> nodeLookupMap;
    private Map<Object, List<Command<A>>> vertexLingerTriggerMap;
    private MultiMap<ImmutablePair<A, A>, Object> connToEdgeLookupMap;
    private Map<Object, ImmutablePair<A, A>> edgeToConnLookupMap;
    private AtomicReference<VisualizerEventListener> listener = new AtomicReference<>();
    private AtomicReference<Recorder<A>> recorder = new AtomicReference<>();
    private AtomicBoolean consumed = new AtomicBoolean();

    /**
     * Creates a {@link JGraphXVisualizer} object.
     */
    public JGraphXVisualizer() {

        graph = new mxGraph();
        graph.setCellsEditable(false);
        graph.setAllowDanglingEdges(false);
        graph.setAllowLoops(false);
        graph.setCellsDeletable(false);
        graph.setCellsCloneable(false);
        graph.setCellsDisconnectable(false);
        graph.setDropEnabled(false);
        graph.setSplitEnabled(false);
        graph.setCellsBendable(false);
        graph.setConnectableEdges(false);
        graph.setCellsMovable(false);
        graph.setCellsResizable(false);
        graph.setAutoSizeCells(true);

        component = new mxGraphComponent(graph);
        component.setConnectable(false);

        component.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        component.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);

        nodeLookupMap = new DualHashBidiMap<>();
        connToEdgeLookupMap = new MultiValueMap<>();
        edgeToConnLookupMap = new HashMap<>();
        vertexLingerTriggerMap = new HashMap<>();

        textOutputArea = new JTextArea();
        textOutputArea.setLineWrap(false);
        textOutputArea.setEditable(false);
        JScrollPane textOutputScrollPane = new JScrollPane(textOutputArea);
        textOutputScrollPane.setPreferredSize(new Dimension(0, 100));

        frame = new JFrame("Visualizer");
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, component, textOutputScrollPane);
        splitPane.setResizeWeight(1.0);

        frame.setContentPane(splitPane);

        component.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                zoomFit();
            }
        });

        frame.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosed(WindowEvent e) {
                Recorder<A> rec = recorder.get();
                if (rec != null) {
                    IOUtils.closeQuietly(rec);
                }

                VisualizerEventListener veListener = listener.get();
                if (veListener != null) {
                    veListener.closed();
                }
            }
        });

        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

        splitPane.setDividerLocation(0.2);
    }

    @Override
    public void visualize(final Recorder recorder, final VisualizerEventListener listener) {
        if (consumed.getAndSet(true)) {
            throw new IllegalStateException();
        }

        try {
            SwingUtilities.invokeAndWait(() -> {
                textOutputArea.append(JGraphXVisualizer.class.getSimpleName() + " started.\n\n");
                JGraphXVisualizer.this.listener.set(listener);
                JGraphXVisualizer.this.recorder.set(recorder);
                frame.setVisible(true);
            });
        } catch (InterruptedException | InvocationTargetException ex) {
            throw new RuntimeException("Visualize failed", ex);
        }
    }

    @Override
    public void visualize() {
        visualize(null, null);
    }

    @Override
    public void step(String output, Command<A>... commands) {
        Validate.notNull(output);
        Validate.noNullElements(commands);

        String name = Thread.currentThread().getName();
        String dateStr = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);

        textOutputArea.append(dateStr + " / " + name + " - " + output + " \n");
        textOutputArea.setCaretPosition(textOutputArea.getDocument().getLength());

        Recorder<A> rec = this.recorder.get();
        if (rec != null) {
            rec.recordStep(output, commands);
        }

        queueCommands(Arrays.asList(commands));
    }

    private void queueCommands(List<Command<A>> commands) {
        commands.stream().forEach((command) -> {
            if (command instanceof AddNodeCommand) {
                addNode((AddNodeCommand<A>) command);
            } else if (command instanceof RemoveNodeCommand) {
                removeNode((RemoveNodeCommand<A>) command);
            } else if (command instanceof AddEdgeCommand) {
                addConnection((AddEdgeCommand<A>) command);
            } else if (command instanceof RemoveEdgeCommand) {
                removeConnection((RemoveEdgeCommand<A>) command);
            } else if (command instanceof ChangeNodeCommand) {
                changeNode((ChangeNodeCommand<A>) command);
            } else if (command instanceof TriggerOnLingeringNodeCommand) {
                triggerOnLingeringNode((TriggerOnLingeringNodeCommand<A>) command);
            } else {
                textOutputArea.append("  UNKNOWN COMMAND RECIEVED, SKIPPING -- " + command.getClass().getName());
            }
        });
    }

    private void triggerOnLingeringNode(final TriggerOnLingeringNodeCommand<A> command) {
        Validate.notNull(command);

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                A address = command.getNode();

                Object vertex = nodeLookupMap.get(address);
                Validate.isTrue(vertex != null);

                vertexLingerTriggerMap.put(vertex, command.getTriggerCommand());
            }
        });
    }

    private void changeNode(final ChangeNodeCommand<A> command) {
        Validate.notNull(command);

        SwingUtilities.invokeLater(() -> {
            A address = command.getNode();

            Object vertex = nodeLookupMap.get(address);
            Validate.isTrue(vertex != null);

            Point center = command.getLocation();
            if (center != null) {
                mxRectangle rect = graph.getView().getBoundingBox(new Object[] { vertex });
                graph.moveCells(new Object[] { vertex }, center.getX() - rect.getCenterX(),
                        center.getY() - rect.getCenterY());
            }

            Double scale = command.getScale();
            if (scale != null) {
                mxGraphView view = graph.getView();
                mxRectangle rect = graph.getBoundingBox(vertex);

                rect.setWidth(rect.getWidth() / view.getScale() * scale);
                rect.setHeight(rect.getHeight() / view.getScale() * scale);
                rect.setX(rect.getX() / view.getScale());
                rect.setY(rect.getY() / view.getScale());
                graph.resizeCell(vertex, rect);
            }

            Color color = command.getColor();
            if (color != null) {
                graph.setCellStyle(mxConstants.STYLE_FILLCOLOR + "=" + "#"
                        + String.format("%06x", color.getRGB() & 0x00FFFFFF), new Object[] { vertex });
                nodeLookupMap.put(address, vertex);
            }

            zoomFit();
        });
    }

    //    private void resizeNode(final A address, final int width, final int height) {
    //        Validate.notNull(address);
    //        Validate.inclusiveBetween(0, Integer.MAX_VALUE, width);
    //        Validate.inclusiveBetween(0, Integer.MAX_VALUE, height);
    //
    //        SwingUtilities.invokeLater(new Runnable() {
    //
    //            @Override
    //            public void run() {
    //                Object vertex = nodeLookupMap.get(address);
    //                Validate.isTrue(vertex != null);
    //
    //                mxGraphView view = graph.getView();
    //                mxRectangle rect = graph.getBoundingBox(vertex);
    //
    //                rect.setWidth(width);
    //                rect.setHeight(height);
    //                rect.setX(rect.getX() / view.getScale());
    //                rect.setY(rect.getY() / view.getScale());
    //                graph.resizeCell(vertex, rect);
    //
    //                zoomFit();
    //            }
    //        });
    //    }

    private void addNode(final AddNodeCommand<A> command) {
        Validate.notNull(command);
        SwingUtilities.invokeLater(() -> {
            Validate.isTrue(!nodeLookupMap.containsKey(command.getNode()));

            Object parent = graph.getDefaultParent();

            Object vertex = graph.insertVertex(parent, null, command.getNode(), 0, 0, 1, 1);
            graph.updateCellSize(vertex);
            graph.moveCells(new Object[] { vertex }, 0, 0);

            mxGraphView view = graph.getView();
            view.validate();

            nodeLookupMap.put(command.getNode(), vertex);

            zoomFit();
        });
    }

    private void removeNode(final RemoveNodeCommand<A> command) {
        Validate.notNull(command);

        SwingUtilities.invokeLater(() -> {
            Validate.isTrue(nodeLookupMap.containsKey(command.getNode()));

            Object vertex = nodeLookupMap.remove(command.getNode());
            Object[] edges = graph.getEdges(vertex);

            for (Object edge : edges) {
                ImmutablePair<A, A> conn = edgeToConnLookupMap.get(edge);
                connToEdgeLookupMap.removeMapping(conn, edge);
            }

            graph.getModel().remove(vertex);

            triggerIfNoEdges(command.getNode(), vertex);

            zoomFit();
        });
    }

    private void addConnection(final AddEdgeCommand<A> command) {
        Validate.notNull(command);

        SwingUtilities.invokeLater(() -> {
            Object parent = graph.getDefaultParent();

            Object fromVertex = nodeLookupMap.get(command.getFrom());
            Object toVertex = nodeLookupMap.get(command.getTo());
            ImmutablePair<A, A> conn = new ImmutablePair<>(command.getFrom(), command.getTo());
            Validate.isTrue(nodeLookupMap.containsKey(command.getFrom()), "Connection %s source doesn't exist",
                    conn);
            Validate.isTrue(nodeLookupMap.containsKey(command.getTo()), "Connection %s destination doesn't exist",
                    conn);
            Validate.isTrue(!connToEdgeLookupMap.containsKey(conn), "Connection %s already exists", conn);

            if (!connToEdgeLookupMap.containsKey(conn)) {
                Object edge = graph.insertEdge(parent, null, null, fromVertex, toVertex);
                connToEdgeLookupMap.put(conn, edge);
                edgeToConnLookupMap.put(edge, conn);
            }

            zoomFit();
        });
    }

    private void removeConnection(final RemoveEdgeCommand<A> command) {
        Validate.notNull(command);

        SwingUtilities.invokeLater(() -> {
            Object fromVertex = nodeLookupMap.get(command.getFrom());
            Object toVertex = nodeLookupMap.get(command.getTo());
            ImmutablePair<A, A> conn = new ImmutablePair<>(command.getFrom(), command.getTo());
            Validate.isTrue(nodeLookupMap.containsKey(command.getFrom()), "Connection %s source doesn't exist",
                    conn);
            Validate.isTrue(nodeLookupMap.containsKey(command.getTo()), "Connection %s destination doesn't exist",
                    conn);
            Validate.isTrue(connToEdgeLookupMap.containsKey(conn), "Connection %s doesn't exists", conn);

            if (fromVertex == null || toVertex == null) {
                return;
            }

            Collection<Object> edges = (Collection<Object>) connToEdgeLookupMap.get(conn);
            Object edge = edges.iterator().next();

            connToEdgeLookupMap.removeMapping(conn, edge);
            edgeToConnLookupMap.remove(edge);

            graph.getModel().remove(edge);

            triggerIfNoEdges(command.getFrom(), fromVertex);
            triggerIfNoEdges(command.getTo(), toVertex);

            zoomFit();
        });
    }

    private void triggerIfNoEdges(A node, Object vertex) { // NOPMD
        Object[] in = graph.getIncomingEdges(vertex);
        Object[] out = graph.getOutgoingEdges(vertex);
        if (in.length == 0 && out.length == 0) {
            List<Command<A>> commands = vertexLingerTriggerMap.remove(vertex);
            if (commands != null) {
                queueCommands(commands);
            }
        }
    }

    private void zoomFit() {
        SwingUtilities.invokeLater(() -> {
            double compWidth = component.getWidth();
            double compHeight = component.getHeight();
            double compLen = Math.min(compWidth, compHeight);

            mxGraphView view = graph.getView();
            double oldScale = view.getScale();
            mxPoint oldTranslate = view.getTranslate();

            mxRectangle graphBounds = view.getGraphBounds();
            double graphX = (graphBounds.getX()) / oldScale - oldTranslate.getX();
            double graphY = (graphBounds.getY()) / oldScale - oldTranslate.getY();
            double graphWidth = (graphBounds.getWidth()) / oldScale;
            double graphHeight = (graphBounds.getHeight()) / oldScale;
            double graphEndX = graphX + graphWidth;
            double graphEndY = graphY + graphHeight;

            double viewLen = Math.max(graphEndX, graphEndY);

            if (Double.isInfinite(viewLen) || Double.isNaN(viewLen) || viewLen <= 0.0) {
                view.setScale(1.0);
            } else {
                double newScale = compLen / viewLen;
                view.setScale(newScale);
            }
        });
    }
}