de.bund.bfr.knime.gis.views.canvas.Canvas.java Source code

Java tutorial

Introduction

Here is the source code for de.bund.bfr.knime.gis.views.canvas.Canvas.java

Source

/*******************************************************************************
 * Copyright (c) 2016 German Federal Institute for Risk Assessment (BfR)
 *
 * 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
 *******************************************************************************/
package de.bund.bfr.knime.gis.views.canvas;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;

import com.google.common.base.Strings;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;

import de.bund.bfr.jung.BetterEdgeShapeTransformer;
import de.bund.bfr.jung.BetterGraphMouse;
import de.bund.bfr.jung.BetterPickingGraphMousePlugin;
import de.bund.bfr.jung.BetterScalingGraphMousePlugin;
import de.bund.bfr.jung.BetterVisualizationViewer;
import de.bund.bfr.jung.JungListener;
import de.bund.bfr.jung.JungUtils;
import de.bund.bfr.jung.MiddleEdgeArrowRenderingSupport;
import de.bund.bfr.jung.ZoomingPaintable;
import de.bund.bfr.knime.KnimeUtils;
import de.bund.bfr.knime.gis.views.canvas.dialogs.DefaultPropertySelectorCreator;
import de.bund.bfr.knime.gis.views.canvas.dialogs.HighlightDialog;
import de.bund.bfr.knime.gis.views.canvas.dialogs.HighlightListDialog;
import de.bund.bfr.knime.gis.views.canvas.dialogs.HighlightSelectionDialog;
import de.bund.bfr.knime.gis.views.canvas.dialogs.PropertiesDialog;
import de.bund.bfr.knime.gis.views.canvas.dialogs.PropertySelectorCreator;
import de.bund.bfr.knime.gis.views.canvas.dialogs.SinglePropertiesDialog;
import de.bund.bfr.knime.gis.views.canvas.element.Edge;
import de.bund.bfr.knime.gis.views.canvas.element.Element;
import de.bund.bfr.knime.gis.views.canvas.element.Node;
import de.bund.bfr.knime.gis.views.canvas.highlighting.HighlightConditionList;
import de.bund.bfr.knime.gis.views.canvas.util.CanvasLegend;
import de.bund.bfr.knime.gis.views.canvas.util.CanvasOptionsPanel;
import de.bund.bfr.knime.gis.views.canvas.util.CanvasPopupMenu;
import de.bund.bfr.knime.gis.views.canvas.util.EdgePropertySchema;
import de.bund.bfr.knime.gis.views.canvas.util.Naming;
import de.bund.bfr.knime.gis.views.canvas.util.NodePropertySchema;
import de.bund.bfr.knime.gis.views.canvas.util.PropertySchema;
import de.bund.bfr.knime.gis.views.canvas.util.Transform;
import de.bund.bfr.knime.ui.Dialogs;
import de.bund.bfr.knime.ui.ListFilterDialog;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.visualization.Layer;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.VisualizationImageServer;
import edu.uci.ics.jung.visualization.VisualizationServer.Paintable;
import edu.uci.ics.jung.visualization.control.ModalGraphMouse.Mode;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeArrowRenderingSupport;
import edu.uci.ics.jung.visualization.transform.MutableAffineTransformer;

public abstract class Canvas<V extends Node> extends JPanel
        implements JungListener, CanvasPopupMenu.ClickListener, CanvasOptionsPanel.ChangeListener, ICanvas<V> {

    private static final long serialVersionUID = 1L;
    private static final String IS_META_NODE = "IsMeta";

    protected BetterVisualizationViewer<V, Edge<V>> viewer;
    protected Transform transform;
    protected Naming naming;

    protected List<V> allNodes;
    protected List<Edge<V>> allEdges;
    protected Set<V> nodes;
    protected Set<Edge<V>> edges;
    protected Map<String, V> nodeSaveMap;
    protected Map<String, Edge<V>> edgeSaveMap;
    protected Map<Edge<V>, Set<Edge<V>>> joinMap;
    protected Map<String, Set<String>> collapsedNodes;

    protected NodePropertySchema nodeSchema;
    protected EdgePropertySchema edgeSchema;
    protected String metaNodeProperty;

    protected HighlightConditionList nodeHighlightConditions;
    protected HighlightConditionList edgeHighlightConditions;

    private CanvasOptionsPanel optionsPanel;
    private CanvasPopupMenu popup;

    public Canvas(List<V> nodes, List<Edge<V>> edges, NodePropertySchema nodeSchema, EdgePropertySchema edgeSchema,
            Naming naming) {
        this.nodeSchema = nodeSchema;
        this.edgeSchema = edgeSchema;
        this.naming = naming;
        transform = Transform.IDENTITY_TRANSFORM;
        nodeHighlightConditions = new HighlightConditionList();
        edgeHighlightConditions = new HighlightConditionList();

        this.nodes = new LinkedHashSet<>();
        this.edges = new LinkedHashSet<>();
        CanvasUtils.copyNodesAndEdges(nodes, edges, this.nodes, this.edges);
        allNodes = nodes;
        allEdges = edges;
        nodeSaveMap = CanvasUtils.getElementsById(this.nodes);
        edgeSaveMap = CanvasUtils.getElementsById(this.edges);
        joinMap = new LinkedHashMap<>();
        collapsedNodes = new LinkedHashMap<>();
        metaNodeProperty = KnimeUtils.createNewValue(IS_META_NODE, nodeSchema.getMap().keySet());
        nodeSchema.getMap().put(metaNodeProperty, Boolean.class);

        viewer = new BetterVisualizationViewer<>();
        viewer.setBackground(Color.WHITE);
        viewer.addPostRenderPaintable(new PostPaintable());
        viewer.addPostRenderPaintable(createZoomingPaintable());
        viewer.getGraphLayout().setGraph(CanvasUtils.createGraph(viewer, this.nodes, this.edges));

        RenderContext<V, Edge<V>> rc = viewer.getRenderContext();

        rc.setEdgeShapeTransformer(new BetterEdgeShapeTransformer<>(CanvasOptionsPanel.DEFAULT_FONT_SIZE));
        rc.setVertexFillPaintTransformer(JungUtils.newNodeFillTransformer(rc, null));
        rc.setVertexStrokeTransformer(JungUtils.newNodeStrokeTransformer(rc, null));
        rc.setVertexDrawPaintTransformer(JungUtils.newNodeDrawTransformer(rc));
        rc.setEdgeDrawPaintTransformer(JungUtils.newEdgeDrawTransformer(rc));
        rc.setEdgeFillPaintTransformer(JungUtils.newEdgeFillTransformer(rc, null));
        ((MutableAffineTransformer) rc.getMultiLayerTransformer().getTransformer(Layer.LAYOUT))
                .addChangeListener(e -> {
                    AffineTransform transform = ((MutableAffineTransformer) rc.getMultiLayerTransformer()
                            .getTransformer(Layer.LAYOUT)).getTransform();
                    boolean transformValid = transform.getScaleX() != 0.0 && transform.getScaleY() != 0.0;

                    this.transform = transformValid ? new Transform(transform) : Transform.IDENTITY_TRANSFORM;
                    applyTransform();
                });

        BetterGraphMouse<V, Edge<V>> graphMouse = new BetterGraphMouse<>(createPickingPlugin(),
                createScalingPlugin());

        graphMouse.setMode(CanvasOptionsPanel.DEFAULT_MODE);
        graphMouse.addChangeListener(this);

        viewer.setGraphMouse(graphMouse);

        setLayout(new BorderLayout());
        add(viewer, BorderLayout.CENTER);
    }

    @Override
    public JPanel getComponent() {
        return this;
    }

    @Override
    public void addCanvasListener(CanvasListener listener) {
        listenerList.add(CanvasListener.class, listener);
    }

    @Override
    public void removeCanvasListener(CanvasListener listener) {
        listenerList.remove(CanvasListener.class, listener);
    }

    @Override
    public Set<V> getNodes() {
        return nodes;
    }

    @Override
    public Set<Edge<V>> getEdges() {
        return edges;
    }

    @Override
    public Dimension getCanvasSize() {
        return viewer.getSize();
    }

    @Override
    public void setCanvasSize(Dimension canvasSize) {
        viewer.setPreferredSize(canvasSize);
        resetLayoutItemClicked();
    }

    @Override
    public Mode getEditingMode() {
        return optionsPanel.getEditingMode();
    }

    @Override
    public void setEditingMode(Mode editingMode) {
        optionsPanel.setEditingMode(editingMode);
    }

    @Override
    public boolean isShowLegend() {
        return optionsPanel.isShowLegend();
    }

    @Override
    public void setShowLegend(boolean showLegend) {
        optionsPanel.setShowLegend(showLegend);
    }

    @Override
    public boolean isJoinEdges() {
        return optionsPanel.isJoinEdges();
    }

    @Override
    public void setJoinEdges(boolean joinEdges) {
        optionsPanel.setJoinEdges(joinEdges);
    }

    @Override
    public boolean isSkipEdgelessNodes() {
        return optionsPanel.isSkipEdgelessNodes();
    }

    @Override
    public void setSkipEdgelessNodes(boolean skipEdgelessNodes) {
        optionsPanel.setSkipEdgelessNodes(skipEdgelessNodes);
    }

    @Override
    public boolean isShowEdgesInMetaNode() {
        return optionsPanel.isShowEdgesInMetaNode();
    }

    @Override
    public void setShowEdgesInMetaNode(boolean showEdgesInMetaNode) {
        optionsPanel.setShowEdgesInMetaNode(showEdgesInMetaNode);
    }

    @Override
    public int getFontSize() {
        return optionsPanel.getFontSize();
    }

    @Override
    public void setFontSize(int fontSize) {
        optionsPanel.setFontSize(fontSize);
    }

    @Override
    public boolean isFontBold() {
        return optionsPanel.isFontBold();
    }

    @Override
    public void setFontBold(boolean fontBold) {
        optionsPanel.setFontBold(fontBold);
    }

    @Override
    public int getNodeSize() {
        return optionsPanel.getNodeSize();
    }

    @Override
    public void setNodeSize(int nodeSize) {
        optionsPanel.setNodeSize(nodeSize);
    }

    @Override
    public Integer getNodeMaxSize() {
        return optionsPanel.getNodeMaxSize();
    }

    @Override
    public void setNodeMaxSize(Integer nodeMaxSize) {
        optionsPanel.setNodeMaxSize(nodeMaxSize);
    }

    @Override
    public int getEdgeThickness() {
        return optionsPanel.getEdgeThickness();
    }

    @Override
    public void setEdgeThickness(int edgeThickness) {
        optionsPanel.setEdgeThickness(edgeThickness);
    }

    @Override
    public Integer getEdgeMaxThickness() {
        return optionsPanel.getEdgeMaxThickness();
    }

    @Override
    public void setEdgeMaxThickness(Integer edgeMaxThickness) {
        optionsPanel.setEdgeMaxThickness(edgeMaxThickness);
    }

    @Override
    public boolean isArrowInMiddle() {
        return optionsPanel.isArrowInMiddle();
    }

    @Override
    public void setArrowInMiddle(boolean arrowInMiddle) {
        optionsPanel.setArrowInMiddle(arrowInMiddle);
    }

    @Override
    public String getLabel() {
        return optionsPanel.getLabel();
    }

    @Override
    public void setLabel(String label) {
        optionsPanel.setLabel(label);
    }

    @Override
    public int getBorderAlpha() {
        return optionsPanel.getBorderAlpha();
    }

    @Override
    public void setBorderAlpha(int borderAlpha) {
        optionsPanel.setBorderAlpha(borderAlpha);
    }

    @Override
    public boolean isAvoidOverlay() {
        return optionsPanel.isAvoidOverlay();
    }

    @Override
    public void setAvoidOverlay(boolean avoidOverlay) {
        optionsPanel.setAvoidOverlay(avoidOverlay);
    }

    @Override
    public NodePropertySchema getNodeSchema() {
        return nodeSchema;
    }

    @Override
    public EdgePropertySchema getEdgeSchema() {
        return edgeSchema;
    }

    @Override
    public Naming getNaming() {
        return naming;
    }

    @Override
    public Set<V> getSelectedNodes() {
        return new LinkedHashSet<>(Sets.intersection(viewer.getPickedVertexState().getPicked(), nodes));
    }

    @Override
    public void setSelectedNodes(Set<V> selectedNodes) {
        viewer.getPickedVertexState().clear();
        selectedNodes.stream().filter(n -> nodes.contains(n))
                .forEach(n -> viewer.getPickedVertexState().pick(n, true));
        nodePickingFinished();
    }

    @Override
    public Set<Edge<V>> getSelectedEdges() {
        return new LinkedHashSet<>(Sets.intersection(viewer.getPickedEdgeState().getPicked(), edges));
    }

    @Override
    public void setSelectedEdges(Set<Edge<V>> selectedEdges) {
        viewer.getPickedEdgeState().clear();
        selectedEdges.stream().filter(e -> edges.contains(e))
                .forEach(e -> viewer.getPickedEdgeState().pick(e, true));
        edgePickingFinished();
    }

    @Override
    public Set<String> getSelectedNodeIds() {
        return CanvasUtils.getElementIds(getSelectedNodes());
    }

    @Override
    public void setSelectedNodeIds(Set<String> selectedNodeIds) {
        setSelectedNodes(CanvasUtils.getElementsById(nodes, selectedNodeIds));
    }

    @Override
    public void setSelectedNodeIdsWithoutListener(Set<String> selectedNodeIds) {
        viewer.getPickedVertexState().clear();
        nodes.stream().filter(n -> selectedNodeIds.contains(n.getId()))
                .forEach(n -> viewer.getPickedVertexState().pick(n, true));
        popup.setNodeSelectionEnabled(!viewer.getPickedVertexState().getPicked().isEmpty());
    }

    @Override
    public Set<String> getSelectedEdgeIds() {
        return CanvasUtils.getElementIds(getSelectedEdges());
    }

    @Override
    public void setSelectedEdgeIds(Set<String> selectedEdgeIds) {
        setSelectedEdges(CanvasUtils.getElementsById(edges, selectedEdgeIds));
    }

    @Override
    public void setSelectedEdgeIdsWithoutListener(Set<String> selectedEdgeIds) {
        viewer.getPickedEdgeState().clear();
        edges.stream().filter(e -> selectedEdgeIds.contains(e.getId()))
                .forEach(e -> viewer.getPickedEdgeState().pick(e, true));
        popup.setEdgeSelectionEnabled(!viewer.getPickedEdgeState().getPicked().isEmpty());
    }

    @Override
    public HighlightConditionList getNodeHighlightConditions() {
        return nodeHighlightConditions;
    }

    @Override
    public void setNodeHighlightConditions(HighlightConditionList nodeHighlightConditions) {
        this.nodeHighlightConditions = nodeHighlightConditions;
        applyChanges();
        call(l -> l.nodeHighlightingChanged(this));
    }

    @Override
    public HighlightConditionList getEdgeHighlightConditions() {
        return edgeHighlightConditions;
    }

    @Override
    public void setEdgeHighlightConditions(HighlightConditionList edgeHighlightConditions) {
        this.edgeHighlightConditions = edgeHighlightConditions;
        applyChanges();
        call(l -> l.edgeHighlightingChanged(this));
    }

    @Override
    public void setHighlightConditions(HighlightConditionList nodeHighlightConditions,
            HighlightConditionList edgeHighlightConditions) {
        this.nodeHighlightConditions = nodeHighlightConditions;
        this.edgeHighlightConditions = edgeHighlightConditions;
        applyChanges();
        call(l -> l.highlightingChanged(this));
    }

    @Override
    public Map<String, Set<String>> getCollapsedNodes() {
        return collapsedNodes;
    }

    @Override
    public void setCollapsedNodes(Map<String, Set<String>> collapsedNodes) {
        Sets.difference(this.collapsedNodes.keySet(), collapsedNodes.keySet())
                .forEach(id -> nodeSaveMap.remove(id));
        this.collapsedNodes = collapsedNodes;
        applyChanges();
        call(l -> l.collapsedNodesChanged(this));
    }

    @Override
    public Transform getTransform() {
        return transform;
    }

    @Override
    public void setTransform(Transform transform) {
        this.transform = transform;
        ((MutableAffineTransformer) viewer.getRenderContext().getMultiLayerTransformer()
                .getTransformer(Layer.LAYOUT)).setTransform(transform.toAffineTransform());
        applyTransform();
        viewer.repaint();
    }

    @Override
    public void transformFinished() {
        call(l -> l.transformChanged(this));
    }

    @Override
    public void pickingFinished() {
        popup.setNodeSelectionEnabled(!viewer.getPickedVertexState().getPicked().isEmpty());
        popup.setEdgeSelectionEnabled(!viewer.getPickedEdgeState().getPicked().isEmpty());
        call(l -> l.selectionChanged(this));
    }

    @Override
    public void nodePickingFinished() {
        popup.setNodeSelectionEnabled(!viewer.getPickedVertexState().getPicked().isEmpty());
        call(l -> l.nodeSelectionChanged(this));
    }

    @Override
    public void edgePickingFinished() {
        popup.setEdgeSelectionEnabled(!viewer.getPickedEdgeState().getPicked().isEmpty());
        call(l -> l.edgeSelectionChanged(this));
    }

    @Override
    public void nodeMovementFinished() {
        call(l -> l.nodePositionsChanged(this));
    }

    @SuppressWarnings("unchecked")
    @Override
    public void modeChangeFinished() {
        optionsPanel.removeChangeListener(this);
        optionsPanel.setEditingMode(((BetterGraphMouse<V, Edge<V>>) viewer.getGraphMouse()).getMode());
        optionsPanel.addChangeListener(this);
    }

    @Override
    public void doubleClickedOn(Object obj) {
        PropertySchema schema = null;

        if (obj instanceof Node) {
            schema = nodeSchema;
        } else if (obj instanceof Edge) {
            schema = edgeSchema;
        }

        if (schema != null) {
            SinglePropertiesDialog dialog = new SinglePropertiesDialog(viewer, (Element) obj, schema);

            dialog.setVisible(true);
        }
    }

    @Override
    public void resetLayoutItemClicked() {
        setTransform(Transform.IDENTITY_TRANSFORM);
        transformFinished();
    }

    @Override
    public void saveAsItemClicked() {
        File file = Dialogs.showImageFileChooser(this);

        if (file == null) {
            return;
        }

        if (file.getName().toLowerCase().endsWith(".png")) {
            try {
                BufferedImage img = new BufferedImage(viewer.getWidth(), viewer.getHeight(),
                        BufferedImage.TYPE_INT_RGB);

                getVisualizationServer(false).paint(img.getGraphics());
                ImageIO.write(img, "png", file);
            } catch (IOException e) {
                Dialogs.showErrorMessage(this, "Error saving png file");
            }
        } else if (file.getName().toLowerCase().endsWith(".svg")) {
            try (Writer outsvg = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
                SVGGraphics2D g = new SVGGraphics2D(new SVGDOMImplementation().createDocument(null, "svg", null));

                g.setSVGCanvasSize(new Dimension(viewer.getWidth(), viewer.getHeight()));
                getVisualizationServer(true).paint(g);
                g.stream(outsvg, true);
            } catch (IOException e) {
                Dialogs.showErrorMessage(this, "Error saving svg file");
            }
        }
    }

    @Override
    public void selectConnectionsItemClicked() {
        Set<V> selected = getSelectedNodes();

        edges.stream().filter(e -> selected.contains(e.getFrom()) && selected.contains(e.getTo()))
                .forEach(e -> viewer.getPickedEdgeState().pick(e, true));
        edgePickingFinished();
    }

    @Override
    public void selectIncomingItemClicked() {
        Set<V> selected = getSelectedNodes();

        edges.stream().filter(e -> selected.contains(e.getTo()))
                .forEach(e -> viewer.getPickedEdgeState().pick(e, true));
        edgePickingFinished();
    }

    @Override
    public void selectOutgoingItemClicked() {
        Set<V> selected = getSelectedNodes();

        edges.stream().filter(e -> selected.contains(e.getFrom()))
                .forEach(e -> viewer.getPickedEdgeState().pick(e, true));
        edgePickingFinished();
    }

    @Override
    public void clearSelectedNodesItemClicked() {
        viewer.getPickedVertexState().clear();
        nodePickingFinished();
    }

    @Override
    public void clearSelectedEdgesItemClicked() {
        viewer.getPickedEdgeState().clear();
        edgePickingFinished();
    }

    @Override
    public void highlightSelectedNodesItemClicked() {
        HighlightListDialog dialog = openNodeHighlightDialog();

        dialog.setAutoAddCondition(
                CanvasUtils.createIdHighlightCondition(getSelectedNodeIds(), nodeSchema.getId()));
        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setNodeHighlightConditions(dialog.getHighlightConditions());
        }
    }

    @Override
    public void highlightSelectedEdgesItemClicked() {
        HighlightListDialog dialog = openEdgeHighlightDialog();

        dialog.setAutoAddCondition(
                CanvasUtils.createIdHighlightCondition(getSelectedEdgeIds(), edgeSchema.getId()));
        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setEdgeHighlightConditions(dialog.getHighlightConditions());
        }
    }

    @Override
    public void highlightNodesItemClicked() {
        HighlightListDialog dialog = openNodeHighlightDialog();

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setNodeHighlightConditions(dialog.getHighlightConditions());
        }
    }

    @Override
    public void highlightEdgesItemClicked() {
        HighlightListDialog dialog = openEdgeHighlightDialog();

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setEdgeHighlightConditions(dialog.getHighlightConditions());
        }
    }

    @Override
    public void clearHighlightedNodesItemClicked() {
        setNodeHighlightConditions(new HighlightConditionList());
    }

    @Override
    public void clearHighlightedEdgesItemClicked() {
        setEdgeHighlightConditions(new HighlightConditionList());
    }

    @Override
    public void selectHighlightedNodesItemClicked() {
        HighlightSelectionDialog dialog = new HighlightSelectionDialog(this,
                nodeHighlightConditions.getConditions());

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setSelectedNodes(CanvasUtils.getHighlightedElements(nodes, dialog.getHighlightConditions()));
        }
    }

    @Override
    public void selectHighlightedEdgesItemClicked() {
        HighlightSelectionDialog dialog = new HighlightSelectionDialog(this,
                edgeHighlightConditions.getConditions());

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setSelectedEdges(CanvasUtils.getHighlightedElements(edges, dialog.getHighlightConditions()));
        }
    }

    @Override
    public void highlightNodeCategoriesItemClicked() {
        String result = Dialogs.showInputDialog(this, "Select Property with Categories?", "Highlight Categories",
                nodeSchema.getMap().keySet());

        if (result != null) {
            nodeHighlightConditions.getConditions().addAll(CanvasUtils.createCategorialHighlighting(nodes, result));
            setNodeHighlightConditions(nodeHighlightConditions);
        }
    }

    @Override
    public void highlightEdgeCategoriesItemClicked() {
        String result = Dialogs.showInputDialog(this, "Select Property with Categories?", "Highlight Categories",
                edgeSchema.getMap().keySet());

        if (result != null) {
            edgeHighlightConditions.getConditions().addAll(CanvasUtils.createCategorialHighlighting(edges, result));
            setEdgeHighlightConditions(edgeHighlightConditions);
        }
    }

    @Override
    public void selectNodesItemClicked() {
        nodeSchema.getPossibleValues().clear();
        nodeSchema.getPossibleValues().putAll(CanvasUtils.getPossibleValues(nodeSaveMap.values()));

        HighlightDialog dialog = HighlightDialog.createFilterDialog(this, nodeSchema, null,
                createPropertySelectorCreator());

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setSelectedNodes(
                    CanvasUtils.getHighlightedElements(nodes, Arrays.asList(dialog.getHighlightCondition())));
        }
    }

    @Override
    public void selectEdgesItemClicked() {
        edgeSchema.getPossibleValues().clear();
        edgeSchema.getPossibleValues().putAll(CanvasUtils.getPossibleValues(edgeSaveMap.values()));

        HighlightDialog dialog = HighlightDialog.createFilterDialog(this, edgeSchema, null,
                createPropertySelectorCreator());

        dialog.setVisible(true);

        if (dialog.isApproved()) {
            setSelectedEdges(
                    CanvasUtils.getHighlightedElements(edges, Arrays.asList(dialog.getHighlightCondition())));
        }
    }

    @Override
    public void nodePropertiesItemClicked() {
        PropertiesDialog<V> dialog = PropertiesDialog.createNodeDialog(this, getSelectedNodes(), nodeSchema, true);

        dialog.setVisible(true);
    }

    @Override
    public void edgePropertiesItemClicked() {
        PropertiesDialog<V> dialog = PropertiesDialog.createEdgeDialog(this, getSelectedEdges(), edgeSchema, true);

        dialog.setVisible(true);
    }

    @Override
    public void nodeAllPropertiesItemClicked() {
        Set<V> pickedAll = new LinkedHashSet<>();

        for (V node : getSelectedNodes()) {
            if (collapsedNodes.containsKey(node.getId())) {
                pickedAll.addAll(CanvasUtils.getElementsById(nodeSaveMap, collapsedNodes.get(node.getId())));
            } else {
                pickedAll.add(node);
            }
        }

        PropertiesDialog<V> dialog = PropertiesDialog.createNodeDialog(this, pickedAll, nodeSchema, false);

        dialog.setVisible(true);
    }

    @Override
    public void edgeAllPropertiesItemClicked() {
        Set<Edge<V>> allPicked = new LinkedHashSet<>();

        for (Edge<V> p : getSelectedEdges()) {
            if (joinMap.containsKey(p)) {
                allPicked.addAll(joinMap.get(p));
            } else {
                allPicked.add(p);
            }
        }

        PropertiesDialog<V> dialog = PropertiesDialog.createEdgeDialog(this, allPicked, edgeSchema, false);

        dialog.setVisible(true);
    }

    @Override
    public void collapseToNodeItemClicked() {
        Set<String> selectedIds = getSelectedNodeIds();
        Set<String> selectedMetaIds = new LinkedHashSet<>(Sets.intersection(selectedIds, collapsedNodes.keySet()));

        if (!selectedMetaIds.isEmpty()) {
            String message = "Some of the selected " + naming.nodes() + " are already meta " + naming.nodes()
                    + ".\nCollapse all contained " + naming.nodes() + " into the new meta " + naming.node() + "?";

            if (Dialogs.showOkCancelDialog(this, message, "Confirm") == Dialogs.OkCancelResult.CANCEL) {
                return;
            }
        }

        String newId = null;

        while (true) {
            newId = Dialogs.showInputDialog(this, "Specify ID for Meta " + naming.Node(), naming.Node() + " ID",
                    "");

            if (newId == null) {
                return;
            } else if (!nodeSaveMap.containsKey(newId)) {
                break;
            }

            Dialogs.showErrorMessage(this, "ID already exists, please specify different ID");
        }

        for (String id : selectedMetaIds) {
            selectedIds.remove(id);
            selectedIds.addAll(collapsedNodes.remove(id));
            nodeSaveMap.remove(id);
        }

        collapsedNodes.put(newId, selectedIds);
        applyChanges();
        setSelectedNodeIdsWithoutListener(new LinkedHashSet<>(Arrays.asList(newId)));
        call(l -> l.collapsedNodesAndPickingChanged(this));
    }

    @Override
    public void expandFromNodeItemClicked() {
        Set<String> selectedIds = getSelectedNodeIds();

        if (!Sets.difference(selectedIds, collapsedNodes.keySet()).isEmpty()) {
            Dialogs.showErrorMessage(this, "Some of the selected " + naming.nodes() + " are not collapsed");
            return;
        }

        Set<String> newIds = new LinkedHashSet<>();

        for (String id : selectedIds) {
            newIds.addAll(collapsedNodes.remove(id));
            nodeSaveMap.remove(id);
        }

        applyChanges();
        setSelectedNodeIdsWithoutListener(newIds);
        call(l -> l.collapsedNodesAndPickingChanged(this));
    }

    @Override
    public void collapseByPropertyItemClicked() {
        Set<String> selectedIds = CanvasUtils.getElementIds(getSelectedNodes());
        Set<String> idsToCollapse;

        if (!selectedIds.isEmpty()) {
            switch (Dialogs.showYesNoCancelDialog(this,
                    "Use only the selected " + naming.nodes() + " for collapsing?", "Confirm")) {
            case YES:
                idsToCollapse = selectedIds;
                break;
            case NO:
                idsToCollapse = CanvasUtils.getElementIds(getNodes());
                break;
            case CANCEL:
            default:
                return;
            }
        } else {
            idsToCollapse = CanvasUtils.getElementIds(getNodes());
        }

        for (String id : idsToCollapse) {
            if (collapsedNodes.keySet().contains(id)) {
                String message;

                if (idsToCollapse == selectedIds) {
                    message = "Some of the selected " + naming.nodes() + " are already collapsed.";
                } else {
                    message = "Some of the " + naming.nodes()
                            + " are already collapsed. You have to clear all collapsed " + naming.nodes()
                            + " before.";
                }

                Dialogs.showErrorMessage(this, message);
                return;
            }
        }

        String result = Dialogs.showInputDialog(this, "Select Property for Collapse?", "Collapse by Property",
                nodeSchema.getMap().keySet());

        if (result == null) {
            return;
        }

        SetMultimap<Object, V> nodesByProperty = LinkedHashMultimap.create();

        for (String id : idsToCollapse) {
            V node = nodeSaveMap.get(id);
            Object value = node.getProperties().get(result);

            if (value != null) {
                nodesByProperty.put(value, node);
            }
        }

        ListFilterDialog<Object> dialog = new ListFilterDialog<>(this,
                KnimeUtils.ORDERING.sortedCopy(nodesByProperty.keySet()));

        dialog.setVisible(true);

        if (!dialog.isApproved() || dialog.getFiltered().isEmpty()) {
            return;
        }

        nodesByProperty.keySet().retainAll(dialog.getFiltered());

        Set<String> newCollapsedIds = new LinkedHashSet<>();

        Multimaps.asMap(nodesByProperty).forEach((property, nodes) -> {
            String newId = KnimeUtils.createNewValue(property.toString(), nodeSaveMap.keySet());

            collapsedNodes.put(newId, CanvasUtils.getElementIds(nodes));
            newCollapsedIds.add(newId);
        });

        applyChanges();
        setSelectedNodeIdsWithoutListener(newCollapsedIds);
        call(l -> l.collapsedNodesAndPickingChanged(this));
    }

    @Override
    public void clearCollapsedNodesItemClicked() {
        nodeSaveMap.keySet().removeAll(collapsedNodes.keySet());
        collapsedNodes.clear();
        applyChanges();
        popup.setNodeSelectionEnabled(false);
        call(l -> l.collapsedNodesAndPickingChanged(this));
    }

    @SuppressWarnings("unchecked")
    @Override
    public void editingModeChanged() {
        ((BetterGraphMouse<V, Edge<V>>) viewer.getGraphMouse()).setMode(optionsPanel.getEditingMode());
    }

    @Override
    public void showLegendChanged() {
        viewer.repaint();
        call(l -> l.showLegendChanged(this));
    }

    @Override
    public void joinEdgesChanged() {
        applyChanges();
        call(l -> l.edgeJoinChanged(this));
    }

    @Override
    public void skipEdgelessNodesChanged() {
        applyChanges();
        call(l -> l.skipEdgelessChanged(this));
    }

    @Override
    public void showEdgesInMetaNodeChanged() {
        applyChanges();
        call(l -> l.showEdgesInMetaNodeChanged(this));
    }

    @Override
    public void fontChanged() {
        Font font = new Font("default", optionsPanel.isFontBold() ? Font.BOLD : Font.PLAIN, getFontSize());

        viewer.getRenderContext().setEdgeShapeTransformer(new BetterEdgeShapeTransformer<>(getFontSize()));
        viewer.getRenderContext().setVertexFontTransformer(node -> font);
        viewer.getRenderContext().setEdgeFontTransformer(edge -> font);
        viewer.repaint();
        call(l -> l.fontChanged(this));
    }

    @Override
    public void nodeSizeChanged() {
        applyChanges();
        call(l -> l.nodeSizeChanged(this));
    }

    @Override
    public void edgeThicknessChanged() {
        applyChanges();
        call(l -> l.edgeThicknessChanged(this));
    }

    @Override
    public void arrowInMiddleChanged() {
        viewer.getRenderer().getEdgeRenderer().setEdgeArrowRenderingSupport(
                optionsPanel.isArrowInMiddle() ? new MiddleEdgeArrowRenderingSupport<>()
                        : new BasicEdgeArrowRenderingSupport<>());
        viewer.repaint();
        call(l -> l.arrowInMiddleChanged(this));
    }

    @Override
    public void labelChanged() {
        viewer.repaint();
        call(l -> l.labelChanged(this));
    }

    @Override
    public void borderAlphaChanged() {
        call(l -> l.borderAlphaChanged(this));
    }

    @Override
    public void avoidOverlayChanged() {
        call(l -> l.avoidOverlayChanged(this));
    }

    @Override
    public BetterVisualizationViewer<V, Edge<V>> getViewer() {
        return viewer;
    }

    @Override
    public VisualizationImageServer<V, Edge<V>> getVisualizationServer(boolean toSvg) {
        VisualizationImageServer<V, Edge<V>> server = new VisualizationImageServer<>(viewer.getGraphLayout(),
                viewer.getSize());

        server.setBackground(Color.WHITE);
        server.setRenderContext(viewer.getRenderContext());
        server.setRenderer(viewer.getRenderer());
        server.addPostRenderPaintable(new PostPaintable());

        return server;
    }

    @Override
    public CanvasOptionsPanel getOptionsPanel() {
        return optionsPanel;
    }

    @Override
    public void setOptionsPanel(CanvasOptionsPanel optionsPanel) {
        if (this.optionsPanel != null) {
            remove(this.optionsPanel);
        }

        this.optionsPanel = optionsPanel;
        optionsPanel.addChangeListener(this);
        add(optionsPanel, BorderLayout.SOUTH);
        revalidate();
    }

    @Override
    public CanvasPopupMenu getPopupMenu() {
        return popup;
    }

    @Override
    public void setPopupMenu(CanvasPopupMenu popup) {
        this.popup = popup;
        popup.addClickListener(this);
        viewer.setComponentPopupMenu(popup);
    }

    @Override
    public void applyChanges() {
        Set<String> selectedNodeIds = getSelectedNodeIds();
        Set<String> selectedEdgeIds = getSelectedEdgeIds();

        resetNodesAndEdges();
        applyNodeCollapse();
        applyInvisibility();
        applyJoinEdgesAndSkipEdgeless();
        applyShowEdgesInMetaNode();
        applyHighlights();
        viewer.getGraphLayout().setGraph(CanvasUtils.createGraph(viewer, nodes, edges));

        setSelectedNodeIdsWithoutListener(selectedNodeIds);
        setSelectedEdgeIdsWithoutListener(selectedEdgeIds);
        viewer.repaint();
    }

    @Override
    public void resetNodesAndEdges() {
        nodes = CanvasUtils.getElementsById(nodeSaveMap, CanvasUtils.getElementIds(allNodes));
        edges = CanvasUtils.getElementsById(edgeSaveMap, CanvasUtils.getElementIds(allEdges));
    }

    @Override
    public void applyNodeCollapse() {
        Map<String, String> collapseTo = new LinkedHashMap<>();

        collapsedNodes.forEach((to, fromList) -> fromList.forEach(from -> collapseTo.put(from, to)));

        Set<V> newNodes = nodes.stream().filter(n -> !collapseTo.containsKey(n.getId()))
                .collect(Collectors.toCollection(LinkedHashSet::new));

        collapsedNodes.forEach((metaId, containedNodes) -> {
            if (nodeSaveMap.containsKey(metaId)) {
                newNodes.add(nodeSaveMap.get(metaId));
            } else {
                V newNode = createMetaNode(metaId, CanvasUtils.getElementsById(nodeSaveMap, containedNodes));

                nodeSaveMap.put(newNode.getId(), newNode);
                newNodes.add(newNode);
            }
        });

        Set<Edge<V>> newEdges = new LinkedHashSet<>();
        Map<String, V> nodesById = CanvasUtils.getElementsById(newNodes);
        Map<String, Edge<V>> allEdgesById = CanvasUtils.getElementsById(allEdges);

        for (Edge<V> edge : edges) {
            String fromId = allEdgesById.get(edge.getId()).getFrom().getId();
            String toId = allEdgesById.get(edge.getId()).getTo().getId();
            V from = nodesById.get(collapseTo.containsKey(fromId) ? collapseTo.get(fromId) : fromId);
            V to = nodesById.get(collapseTo.containsKey(toId) ? collapseTo.get(toId) : toId);

            if (edge.getFrom().equals(from) && edge.getTo().equals(to)) {
                newEdges.add(edge);
            } else {
                Edge<V> newEdge = new Edge<>(edge.getId(), edge.getProperties(), from, to);

                newEdge.getProperties().put(edgeSchema.getFrom(), from.getId());
                newEdge.getProperties().put(edgeSchema.getTo(), to.getId());
                edgeSaveMap.put(newEdge.getId(), newEdge);
                newEdges.add(newEdge);
            }
        }

        nodes = newNodes;
        edges = newEdges;
    }

    @Override
    public void applyInvisibility() {
        CanvasUtils.removeInvisibleElements(nodes, nodeHighlightConditions);
        CanvasUtils.removeInvisibleElements(edges, edgeHighlightConditions);
        CanvasUtils.removeNodelessEdges(edges, nodes);
    }

    @Override
    public void applyJoinEdgesAndSkipEdgeless() {
        joinMap.clear();

        if (isJoinEdges()) {
            joinMap.putAll(CanvasUtils.joinEdges(edges, edgeSchema, allEdges));
            edges.clear();
            edges.addAll(joinMap.keySet());
        }

        if (isSkipEdgelessNodes()) {
            CanvasUtils.removeEdgelessNodes(nodes, edges);
        }
    }

    @Override
    public void applyHighlights() {
        CanvasUtils.applyNodeHighlights(viewer.getRenderContext(), nodes, nodeHighlightConditions, getNodeSize(),
                getNodeMaxSize(), metaNodeProperty);
        CanvasUtils.applyEdgeHighlights(viewer.getRenderContext(), edges, edgeHighlightConditions,
                getEdgeThickness(), getEdgeMaxThickness());
    }

    @Override
    public void applyShowEdgesInMetaNode() {
        if (!isShowEdgesInMetaNode()) {
            Predicate<Edge<V>> isNotInMetaNode = e -> e.getFrom() != e.getTo()
                    || !collapsedNodes.containsKey(e.getFrom().getId());

            edges = edges.stream().filter(isNotInMetaNode).collect(Collectors.toCollection(LinkedHashSet::new));
        }
    }

    protected Map<String, Point2D> getNodePositions(Collection<V> nodes) {
        Map<String, Point2D> map = new LinkedHashMap<>();
        Layout<V, Edge<V>> layout = viewer.getGraphLayout();

        for (V node : nodes) {
            Point2D pos = layout.transform(node);

            if (pos != null) {
                map.put(node.getId(), new Point2D.Double(pos.getX(), pos.getY()));
            }
        }

        return map;
    }

    protected HighlightListDialog openNodeHighlightDialog() {
        nodeSchema.getPossibleValues().clear();
        nodeSchema.getPossibleValues().putAll(CanvasUtils.getPossibleValues(nodeSaveMap.values()));

        HighlightListDialog dialog = new HighlightListDialog(this, nodeSchema, nodeHighlightConditions);

        dialog.setAllowShape(true);
        dialog.setSelectorCreator(createPropertySelectorCreator());

        return dialog;
    }

    protected HighlightListDialog openEdgeHighlightDialog() {
        edgeSchema.getPossibleValues().clear();
        edgeSchema.getPossibleValues().putAll(CanvasUtils.getPossibleValues(edgeSaveMap.values()));

        HighlightListDialog dialog = new HighlightListDialog(this, edgeSchema, edgeHighlightConditions);

        dialog.setSelectorCreator(createPropertySelectorCreator());
        dialog.addChecker(condition -> {
            if (isJoinEdges() && condition != null && condition.isInvisible()
                    && CanvasUtils.getUsedProperties(condition).contains(edgeSchema.getId())) {
                return "Joined " + naming.edges() + " cannot be made invisible.\nYou can uncheck \"Join "
                        + naming.Edges() + "\" and make the unjoined " + naming.edges() + " invisible.";
            }

            return null;
        });

        return dialog;
    }

    protected PropertySelectorCreator createPropertySelectorCreator() {
        return new DefaultPropertySelectorCreator();
    }

    protected BetterPickingGraphMousePlugin<V, Edge<V>> createPickingPlugin() {
        return new BetterPickingGraphMousePlugin<>(true);
    }

    protected BetterScalingGraphMousePlugin createScalingPlugin() {
        return new BetterScalingGraphMousePlugin(1 / 1.1f, 1.1f);
    }

    protected ZoomingPaintable createZoomingPaintable() {
        ZoomingPaintable zoom = new ZoomingPaintable(viewer, 1.2);

        zoom.addChangeListener(this);

        return zoom;
    }

    protected abstract void applyTransform();

    protected abstract V createMetaNode(String id, Collection<V> nodes);

    private void call(Consumer<CanvasListener> action) {
        Stream.of(getListeners(CanvasListener.class)).forEach(action);
    }

    private class PostPaintable implements Paintable {

        @Override
        public boolean useTransform() {
            return false;
        }

        @Override
        public void paint(Graphics graphics) {
            Graphics2D g = (Graphics2D) graphics;

            if (!Strings.isNullOrEmpty(getLabel())) {
                paintLabel(g);
            }

            if (optionsPanel.isShowLegend()) {
                new CanvasLegend<>(Canvas.this, nodeHighlightConditions, nodes, edgeHighlightConditions, edges)
                        .paint(g);
            }

            Color currentColor = g.getColor();

            g.setColor(Color.BLACK);
            g.drawRect(0, 0, getCanvasSize().width - 1, getCanvasSize().height - 1);
            g.setColor(currentColor);
        }

        private void paintLabel(Graphics2D g) {
            int w = getCanvasSize().width;
            Font font = new Font("Default", Font.BOLD, 10);
            int fontHeight = g.getFontMetrics(font).getHeight();
            int fontAscent = g.getFontMetrics(font).getAscent();
            int dy = 2;

            int dx = 5;
            int sw = (int) font.getStringBounds(getLabel(), g.getFontRenderContext()).getWidth();
            Color currentColor = g.getColor();
            Font currentFont = g.getFont();

            g.setColor(ZoomingPaintable.BACKGROUND);
            g.fillRect(w - sw - 2 * dx, -1, sw + 2 * dx, fontHeight + 2 * dy);
            g.setColor(Color.BLACK);
            g.drawRect(w - sw - 2 * dx, -1, sw + 2 * dx, fontHeight + 2 * dy);
            g.setFont(font);
            g.drawString(getLabel(), w - sw - dx, dy + fontAscent);

            g.setColor(currentColor);
            g.setFont(currentFont);
        }
    }
}