it.unibo.alchemist.boundary.monitors.Generic2DDisplay.java Source code

Java tutorial

Introduction

Here is the source code for it.unibo.alchemist.boundary.monitors.Generic2DDisplay.java

Source

/*
 * Copyright (C) 2010-2015, Danilo Pianini and contributors
 * listed in the project's pom.xml file.
 * 
 * This file is part of Alchemist, and is distributed under the terms of
 * the GNU General Public License, with a linking exception, as described
 * in the file LICENSE in the Alchemist distribution's top directory.
 */
package it.unibo.alchemist.boundary.monitors;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputListener;

import org.apache.commons.math3.util.Pair;
import org.danilopianini.lang.LangUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import it.unibo.alchemist.boundary.gui.effects.Effect;
import it.unibo.alchemist.boundary.interfaces.Graphical2DOutputMonitor;
import it.unibo.alchemist.boundary.l10n.LocalizedResourceBundle;
import it.unibo.alchemist.boundary.wormhole.implementation.AngleManagerImpl;
import it.unibo.alchemist.boundary.wormhole.implementation.ExponentialZoomManager;
import it.unibo.alchemist.boundary.wormhole.implementation.PointAdapter;
import it.unibo.alchemist.boundary.wormhole.implementation.PointerSpeedImpl;
import it.unibo.alchemist.boundary.wormhole.implementation.Wormhole2D;
import it.unibo.alchemist.boundary.wormhole.interfaces.IWormhole2D;
import it.unibo.alchemist.boundary.wormhole.interfaces.IWormhole2D.Mode;
import it.unibo.alchemist.boundary.wormhole.interfaces.PointerSpeed;
import it.unibo.alchemist.boundary.wormhole.interfaces.ZoomManager;
import it.unibo.alchemist.core.interfaces.Simulation;
import it.unibo.alchemist.core.interfaces.Status;
import it.unibo.alchemist.model.implementations.positions.Continuous2DEuclidean;
import it.unibo.alchemist.model.implementations.times.DoubleTime;
import it.unibo.alchemist.model.interfaces.Environment;
import it.unibo.alchemist.model.interfaces.Environment2DWithObstacles;
import it.unibo.alchemist.model.interfaces.Neighborhood;
import it.unibo.alchemist.model.interfaces.Node;
import it.unibo.alchemist.model.interfaces.Obstacle2D;
import it.unibo.alchemist.model.interfaces.Position;
import it.unibo.alchemist.model.interfaces.Reaction;
import it.unibo.alchemist.model.interfaces.Time;

/**
 * Abstract base-class for each display able a graphically represent a 2D space
 * and simulation.
 * 
 * @param <T>
 */
public class Generic2DDisplay<T> extends JPanel implements Graphical2DOutputMonitor<T> {

    static {
        System.setProperty("sun.java2d.opengl", "true");
    }

    /**
     * The default frame rate.
     */
    public static final byte DEFAULT_FRAME_RATE = 25;

    private static final double TIME_STEP = 1d / DEFAULT_FRAME_RATE;
    private static final double FREEDOM_RADIUS = 1d;
    private static final Logger L = LoggerFactory.getLogger(Generic2DDisplay.class);
    private static final int MS_PER_SECOND = 1000;

    /**
     * 
     */
    public static final long PAUSE_DETECTION_THRESHOLD = 200;
    /**
     * How big (in pixels) the selected node should appear.
     */
    private static final byte SELECTED_NODE_DRAWING_SIZE = 16, SELECTED_NODE_INTERNAL_SIZE = 10;
    private static final long serialVersionUID = 511631766719686842L;

    private transient AngleManagerImpl angleManager;
    private Environment<T> currentEnv;
    private List<Effect> effectStack;
    private volatile boolean firstTime = true;
    private boolean paintLinks;
    private transient Optional<Node<T>> hooked = Optional.empty();
    private boolean inited;
    private double lasttime;
    private final Semaphore mapConsistencyMutex = new Semaphore(1);
    private final transient PointerSpeed mouseMovement = new PointerSpeedImpl();
    private int mousex, mousey;
    private Node<T> nearest;
    private final ConcurrentMap<Node<T>, Neighborhood<T>> neighbors = new ConcurrentHashMap<>();
    private List<? extends Obstacle2D> obstacles;
    private final ConcurrentMap<Node<T>, Position> positions = new ConcurrentHashMap<>();
    private boolean realTime;
    private int st;

    private long timeInit = System.currentTimeMillis();

    private transient IWormhole2D wormhole;

    private transient ZoomManager zoomManager;

    private transient boolean isPreviousStateMarking = true;
    private ViewStatus status = ViewStatus.MARK_CLOSER;
    private transient boolean isDraggingMouse;
    private transient Optional<Point> originPoint = Optional.empty();
    private transient Optional<Point> endingPoint = Optional.empty();
    private transient Set<Node<T>> selectedNodes = new HashSet<>();

    /**
     * Initializes a new display with out redrawing the first step.
     */
    public Generic2DDisplay() {
        this(1);
    }

    /**
     * Initializes a new display.
     * 
     * @param step
     *            number of steps to let pass without re-drawing
     */
    public Generic2DDisplay(final int step) {
        super();
        if (!"true".equals(System.getProperty("sun.java2d.opengl"))) {
            L.warn("OpenGL acceleration appears to be disabled on this system. This may impact performance negatively. Please enable it with -Dsun.java2d.opengl=true");
        }
        setStep(step);
        setBackground(Color.WHITE);
        inited = false;
        final MouseManager mgr = new MouseManager();
        addMouseListener(mgr);
        addMouseMotionListener(mgr);
        addMouseWheelListener(mgr);
        bindKeys();
    }

    private boolean isInteracting() {
        return status != ViewStatus.MARK_CLOSER && status != ViewStatus.VIEW_ONLY;
    }

    private void resetStatus() {
        if (isPreviousStateMarking) {
            this.status = ViewStatus.MARK_CLOSER;
        } else {
            this.status = ViewStatus.VIEW_ONLY;
        }
    }

    private void bindKeys() {
        bindKey(KeyEvent.VK_S, () -> {
            if (status == ViewStatus.SELECTING) {
                resetStatus();
                this.selectedNodes.clear();
            } else if (!isInteracting()) {
                this.status = ViewStatus.SELECTING;
            }
            this.repaint();
        });
        bindKey(KeyEvent.VK_O, () -> {
            if (status == ViewStatus.SELECTING) {
                this.status = ViewStatus.MOVING;
            }
        });
        bindKey(KeyEvent.VK_C, () -> {
            if (status == ViewStatus.SELECTING) {
                this.status = ViewStatus.CLONING;
            }
        });
        bindKey(KeyEvent.VK_E, () -> {
            if (status == ViewStatus.SELECTING) {
                this.status = ViewStatus.MOLECULING;
                final JFrame mol = Generic2DDisplay.makeFrame("Moleculing",
                        new MoleculeInjectorGUI<>(selectedNodes));
                mol.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                mol.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosed(final WindowEvent e) {
                        selectedNodes.clear();
                        resetStatus();
                    }
                });
            }
        });
        bindKey(KeyEvent.VK_D, () -> {
            if (status == ViewStatus.SELECTING) {
                this.status = ViewStatus.DELETING;
                for (final Node<T> n : selectedNodes) {
                    currentEnv.removeNode(n);
                }
                final Simulation<T> sim = currentEnv.getSimulation();
                sim.schedule(() -> update(currentEnv, sim.getTime()));
                resetStatus();
            }
        });
        bindKey(KeyEvent.VK_M, () -> setMarkCloserNode(!isCloserNodeMarked()));
        bindKey(KeyEvent.VK_L, () -> setDrawLinks(!paintLinks));
        bindKey(KeyEvent.VK_P, () -> Optional.ofNullable(currentEnv.getSimulation()).ifPresent(sim -> {
            if (sim.getStatus() == Status.RUNNING) {
                sim.pause();
            } else {
                sim.play();
            }
        }));
        bindKey(KeyEvent.VK_R, () -> setRealTime(!isRealTime()));
        bindKey(KeyEvent.VK_LEFT, () -> setStep(Math.max(1, st - Math.max(st / 10, 1))));
        bindKey(KeyEvent.VK_RIGHT, () -> setStep(Math.max(st, st + Math.max(st / 10, 1))));
    }

    private void accessData() {
        mapConsistencyMutex.acquireUninterruptibly();
    }

    private Shape convertObstacle(final Obstacle2D o) {
        final Rectangle2D r = o.getBounds2D();
        final Position[] points = new Position[] { new Continuous2DEuclidean(r.getX(), r.getY()),
                new Continuous2DEuclidean(r.getX() + r.getWidth(), r.getY()),
                new Continuous2DEuclidean(r.getX() + r.getWidth(), r.getY() + r.getHeight()),
                new Continuous2DEuclidean(r.getX(), r.getY() + r.getHeight()) };
        final Path2D path = new GeneralPath();
        for (int i = 0; i < points.length; i++) {
            final Point pt = wormhole.getViewPoint(points[i]);
            if (i == 0) {
                path.moveTo(pt.getX(), pt.getY());
            }
            path.lineTo(pt.getX(), pt.getY());
        }
        path.closePath();
        return path;
    }

    /**
     * This method is meant to be overridden by subclasses that want to display
     * a more sophisticated background than a simple color.
     * 
     * @param g
     *            the Graphics2D to use
     */
    protected void drawBackground(final Graphics2D g) {
    }

    /**
     * Actually draws the environment on the view.
     * 
     * @param g
     *            {@link Graphics2D} object responsible for drawing
     */
    protected final void drawEnvOnView(final Graphics2D g) {
        if (wormhole == null || !isVisible() || !isEnabled()) {
            return;
        }
        accessData();
        if (hooked.isPresent()) {
            final Position hcoor = positions.get(hooked.get());
            final Point hp = wormhole.getViewPoint(hcoor);
            if (hp.distance(getCenter()) > FREEDOM_RADIUS) {
                wormhole.setViewPosition(hp);
            }
        }
        /*
         * Compute nodes in sight and their screen position
         */
        final Map<Node<T>, Point> onView = positions.entrySet().parallelStream()
                .map(pair -> new Pair<>(pair.getKey(), wormhole.getViewPoint(pair.getValue())))
                .filter(p -> wormhole.isInsideView(p.getSecond()))
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
        g.setColor(Color.BLACK);
        if (obstacles != null) {
            /*
             * TODO: only draw obstacles if on view
             */
            obstacles.parallelStream().map(this::convertObstacle).forEachOrdered(g::fill);
        }
        if (paintLinks) {
            g.setColor(Color.GRAY);
            onView.keySet().parallelStream().map(neighbors::get)
                    .flatMap(neigh -> neigh.getNeighbors().parallelStream()
                            .map(node -> node.compareTo(neigh.getCenter()) > 0 ? new Pair<>(neigh.getCenter(), node)
                                    : new Pair<>(node, neigh.getCenter())))
                    .distinct().map(
                            pair -> mapPair(pair,
                                    node -> Optional.ofNullable(onView.get(node))
                                            .orElse(wormhole.getViewPoint(positions.get(node)))))
                    .forEachOrdered(line -> {
                        final Point p1 = line.getFirst();
                        final Point p2 = line.getSecond();
                        g.drawLine(p1.x, p1.y, p2.x, p2.y);
                    });
        }
        releaseData();
        if (isDraggingMouse && status == ViewStatus.MOVING && originPoint.isPresent() && endingPoint.isPresent()) {
            for (final Node<T> n : selectedNodes) {
                if (onView.containsKey(n)) {
                    onView.put(n, new Point(onView.get(n).x + (endingPoint.get().x - originPoint.get().x),
                            onView.get(n).y + (endingPoint.get().y - originPoint.get().y)));
                }
            }
        }
        g.setColor(Color.GREEN);
        if (effectStack != null) {
            effectStack.forEach(effect -> {
                onView.entrySet().forEach(entry -> {
                    final Point p = entry.getValue();
                    effect.apply(g, entry.getKey(), p.x, p.y);
                });
            });
        }
        if (isCloserNodeMarked()) {
            final Optional<Map.Entry<Node<T>, Point>> closest = onView.entrySet().parallelStream()
                    .min((pair1, pair2) -> {
                        final Point p1 = pair1.getValue();
                        final Point p2 = pair2.getValue();
                        final double d1 = Math.hypot(p1.x - mousex, p1.y - mousey);
                        final double d2 = Math.hypot(p2.x - mousex, p2.y - mousey);
                        return Double.compare(d1, d2);
                    });
            if (closest.isPresent()) {
                nearest = closest.get().getKey();
                final int nearestx = closest.get().getValue().x;
                final int nearesty = closest.get().getValue().y;
                drawFriedEgg(g, nearestx, nearesty, Color.RED, Color.YELLOW);
            }
        } else {
            nearest = null;
        }
        if (isDraggingMouse && status == ViewStatus.SELECTING && originPoint.isPresent()
                && endingPoint.isPresent()) {
            g.setColor(Color.BLACK);
            final int x = originPoint.get().x < endingPoint.get().x ? originPoint.get().x : endingPoint.get().x;
            final int y = originPoint.get().y < endingPoint.get().y ? originPoint.get().y : endingPoint.get().y;
            final int width = Math.abs(endingPoint.get().x - originPoint.get().x);
            final int height = Math.abs(endingPoint.get().y - originPoint.get().y);
            g.drawRect(x, y, width, height);
            selectedNodes = onView.entrySet().parallelStream()
                    .filter(nodes -> isInsideRectangle(nodes.getValue(), x, y, width, height))
                    .map(onScreen -> onScreen.getKey()).collect(Collectors.toSet());
        }
        selectedNodes.parallelStream().map(e -> Optional.ofNullable(onView.get(e))).filter(Optional::isPresent)
                .map(Optional::get).forEachOrdered(p -> drawFriedEgg(g, p.x, p.y, Color.BLUE, Color.CYAN));
    }

    private void drawFriedEgg(final Graphics g, final int x, final int y, final Color c1, final Color c2) {
        g.setColor(c1);
        g.fillOval(x - SELECTED_NODE_DRAWING_SIZE / 2, y - SELECTED_NODE_DRAWING_SIZE / 2,
                SELECTED_NODE_DRAWING_SIZE, SELECTED_NODE_DRAWING_SIZE);
        g.setColor(c2);
        g.fillOval(x - SELECTED_NODE_INTERNAL_SIZE / 2, y - SELECTED_NODE_INTERNAL_SIZE / 2,
                SELECTED_NODE_INTERNAL_SIZE, SELECTED_NODE_INTERNAL_SIZE);
    }

    @Override
    public void finished(final Environment<T> environment, final Time time, final long step) {
        update(environment, time);
        firstTime = true;
    }

    /**
     * Gets the view center point.
     * 
     * @return the center
     */
    private Point getCenter() {
        return new Point(getWidth() / 2, getHeight() / 2);
    }

    @Override
    public int getStep() {
        return st;
    }

    /**
     * Lets child-classes access the wormhole.
     * 
     * @return an {@link IWormhole2D}
     */
    protected final IWormhole2D getWormhole() {
        return wormhole;
    }

    /**
     * Lets child-classes access the zoom manager.
     * 
     * @return an {@link ZoomManager}
     */
    protected final ZoomManager getZoomManager() {
        return zoomManager;
    }

    /*
     * Initializes all the internal data.
     */
    private void initAll(final Environment<T> env) {
        wormhole = new Wormhole2D(env, this);
        wormhole.center();
        wormhole.optimalZoom();
        angleManager = new AngleManagerImpl(AngleManagerImpl.DEF_DEG_PER_PIXEL);
        zoomManager = new ExponentialZoomManager(wormhole.getZoom(), ExponentialZoomManager.DEF_BASE);
        if (env instanceof Environment2DWithObstacles) {
            loadObstacles(env);
        } else {
            obstacles = null;
        }
    }

    @Override
    public void initialized(final Environment<T> environment) {
        stepDone(environment, null, new DoubleTime(), 0);
    }

    /**
     * @return true if the closer node is marked
     */
    protected final boolean isCloserNodeMarked() {
        return status == ViewStatus.MARK_CLOSER;
    }

    /**
     * Lets child-classes check if the display is initialized.
     * 
     * @return a <code>boolean</code> value
     */
    protected boolean isInitilized() {
        return inited;
    }

    /**
     * @return true if this monitor is trying to draw in realtime
     */
    @Override
    public final boolean isRealTime() {
        return realTime;
    }

    private void loadObstacles(final Environment<T> env) {
        obstacles = ((Environment2DWithObstacles<?, ?>) env).getObstacles();
    }

    @Override
    protected void paintComponent(final Graphics g) {
        super.paintComponent(g);
        drawBackground((Graphics2D) g);
        drawEnvOnView((Graphics2D) g);
    }

    private void releaseData() {
        mapConsistencyMutex.release();
    }

    /**
     * 
     * @param x x coord
     * @param y y coord
     */
    protected void setDist(final int x, final int y) {
        if (wormhole != null) {
            mousex = x;
            mousey = y;
            final Position envMouse = wormhole.getEnvPoint(new Point(mousex, mousey));
            final StringBuilder sb = new StringBuilder();
            sb.append(envMouse);
            if (nearest != null) {
                sb.append(" -- ");
                sb.append(LocalizedResourceBundle.getString("nearest_node_is"));
                sb.append(": ");
                sb.append(nearest.getId());
            }
            setToolTipText(sb.toString());
        }
    }

    @Override
    public void setDrawLinks(final boolean b) {
        if (paintLinks != b) {
            paintLinks = b;
            repaint();
        }
    }

    @Override
    public void setEffectStack(final List<Effect> l) {
        effectStack = l;
    }

    @Override
    public void setMarkCloserNode(final boolean mark) {
        if (!isInteracting()) {
            if (mark) {
                isPreviousStateMarking = true;
                status = ViewStatus.MARK_CLOSER;
            } else {
                isPreviousStateMarking = false;
                status = ViewStatus.VIEW_ONLY;
            }
            repaint();
        }
    }

    @Override
    public void setRealTime(final boolean rt) {
        realTime = rt;
    }

    @Override
    public final void setStep(final int step) {
        if (step <= 0) {
            throw new IllegalArgumentException("The parameter must be a positive integer");
        }
        st = step;
    }

    /**
     * Lets child-classes change the wormhole.
     * 
     * @param w
     *            an {@link IWormhole2D}
     */
    protected void setWormhole(final IWormhole2D w) {
        Objects.requireNonNull(w);
        wormhole = w;
    }

    /**
     * Lets child-classes change the zoom manager.
     * 
     * @param zm
     *            an {@link ZoomManager}
     */
    protected void setZoomManager(final ZoomManager zm) {
        zoomManager = zm;
        wormhole.setZoom(zoomManager.getZoom());
    }

    @Override
    public void stepDone(final Environment<T> environment, final Reaction<T> r, final Time time, final long step) {
        if (firstTime) {
            synchronized (this) {
                if (firstTime) {
                    initAll(environment);
                    lasttime = -TIME_STEP;
                    firstTime = false;
                    timeInit = System.currentTimeMillis();
                    update(environment, time);
                }
            }
        } else if (st < 1 || step % st == 0) {
            if (isRealTime()) {
                if (lasttime + TIME_STEP > time.toDouble()) {
                    return;
                }
                final long timeSimulated = (long) (time.toDouble() * MS_PER_SECOND);
                if (timeSimulated == 0) {
                    timeInit = System.currentTimeMillis();
                }
                final long timePassed = System.currentTimeMillis() - timeInit;
                if (timePassed - timeSimulated > PAUSE_DETECTION_THRESHOLD) {
                    timeInit = timeInit + timePassed - timeSimulated;
                }
                if (timeSimulated > timePassed) {
                    try {
                        Thread.sleep(Math.min(timeSimulated - timePassed, MS_PER_SECOND / DEFAULT_FRAME_RATE));
                    } catch (final InterruptedException e) {
                        L.warn("Damn spurious wakeups.");
                    }
                }
            }
            update(environment, time);
        }
    }

    private void update(final Environment<T> env, final Time time) {
        if (Thread.holdsLock(env)) {
            if (envHasMobileObstacles(env)) {
                loadObstacles(env);
            }
            lasttime = time.toDouble();
            currentEnv = env;
            accessData();
            positions.clear();
            neighbors.clear();
            env.getNodes().parallelStream().forEach(node -> {
                positions.put(node, env.getPosition(node));
                try {
                    neighbors.put(node, env.getNeighborhood(node).clone());
                } catch (Exception e) {
                    L.error("Unable to clone neighborhood for " + node, e);
                }
            });
            releaseData();
            repaint();
        } else {
            throw new IllegalStateException("Only the simulation thread can dictate GUI updates");
        }
    }

    @Override
    public void zoomTo(final Position center, final double zoomLevel) {
        assert center.getDimensions() == 2;
        wormhole.zoomOnPoint(wormhole.getViewPoint(center), zoomLevel);
    }

    /**
     * @param env
     *            the current environment
     * @param <N>
     *            positions
     * @param <D>
     *            distances
     * @return true if env is subclass of {@link Environment2DWithObstacles}
     *         and has mobile obstacles
     */
    protected static <N extends Number, D extends Number> boolean envHasMobileObstacles(final Environment<?> env) {
        return env instanceof Environment2DWithObstacles
                && ((Environment2DWithObstacles<?, ?>) env).hasMobileObstacles();
    }

    private static <I, O> Pair<O, O> mapPair(final Pair<? extends I, ? extends I> pair,
            final Function<? super I, ? extends O> converter) {
        return new Pair<>(converter.apply(pair.getFirst()), converter.apply(pair.getSecond()));
    }

    private class MouseManager implements MouseInputListener, MouseWheelListener, MouseMotionListener {
        @Override
        public void mouseClicked(final MouseEvent e) {
            setDist(e.getX(), e.getY());
            if (isCloserNodeMarked() && nearest != null && SwingUtilities.isLeftMouseButton(e)
                    && e.getClickCount() == 2) {
                final NodeTracker<T> monitor = new NodeTracker<>(nearest);
                monitor.stepDone(currentEnv, null, new DoubleTime(lasttime), st);
                final Simulation<T> sim = currentEnv.getSimulation();
                final JFrame frame = makeFrame("Tracker for node " + nearest.getId(), monitor);
                if (sim != null) {
                    sim.addOutputMonitor(monitor);
                    frame.addWindowListener(new WindowAdapter() {
                        @Override
                        public void windowClosing(final WindowEvent e) {
                            sim.removeOutputMonitor(monitor);
                        }
                    });
                }
            } else if (status == ViewStatus.CLONING && SwingUtilities.isLeftMouseButton(e)
                    && e.getClickCount() == 1) {
                final Simulation<T> engine = currentEnv.getSimulation();
                final Position envEnding = wormhole.getEnvPoint(e.getPoint());
                engine.schedule(() -> {
                    final Collection<Node<T>> newNodes = new ArrayList<>(selectedNodes.size());
                    try {
                        for (final Node<T> n : selectedNodes) {
                            newNodes.add(n.cloneNode(engine.getTime()));
                        }
                        for (final Node<T> n : newNodes) {
                            currentEnv.addNode(n, envEnding);
                        }
                        update(currentEnv, engine.getTime());
                    } catch (RuntimeException exp) {
                        final String title = "Node cloning error";
                        final String message = "One or more of your nodes do not support cloning, the debug information is:\n"
                                + LangUtils.stackTraceToString(exp);
                        // TODO: switch to JavaFX alerts
                        JOptionPane.showMessageDialog(Generic2DDisplay.this, message, title,
                                JOptionPane.ERROR_MESSAGE);
                    }
                });
                selectedNodes.clear();
                resetStatus();
            }
            if (nearest != null && SwingUtilities.isMiddleMouseButton(e)) {
                hooked = hooked.isPresent() ? Optional.empty() : Optional.of(nearest);
            }
            repaint();
        }

        @Override
        public void mouseDragged(final MouseEvent e) {
            setDist(e.getX(), e.getY());
            if (wormhole == null || mouseMovement == null) {
                return;
            }
            if (SwingUtilities.isLeftMouseButton(e)) {
                if (isDraggingMouse) {
                    endingPoint = Optional.of(e.getPoint());
                }
                if (mouseMovement != null && !hooked.isPresent() && !isInteracting()) {
                    final Point previous = wormhole.getViewPosition();
                    wormhole.setViewPosition(PointAdapter.from(previous)
                            .sum(PointAdapter.from(mouseMovement.getVariation())).toPoint());
                }
            } else if (SwingUtilities.isRightMouseButton(e) && mouseMovement != null && angleManager != null
                    && wormhole.getMode() != Mode.MAP) {
                angleManager.inc(mouseMovement.getVariation().getX());
                wormhole.rotateAroundPoint(getCenter(), angleManager.getAngle());
            }
            mouseMovement.setCurrentPosition(e.getPoint());
            repaint();
        }

        @Override
        public void mouseEntered(final MouseEvent e) {
            updateMouse(e);
        }

        @Override
        public void mouseExited(final MouseEvent e) {
            updateMouse(e);
        }

        @Override
        public void mouseMoved(final MouseEvent e) {
            if (mouseMovement != null) {
                mouseMovement.setCurrentPosition(e.getPoint());
            }
            updateMouse(e);
        }

        @Override
        public void mousePressed(final MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)
                    && (status == ViewStatus.MOVING || status == ViewStatus.SELECTING)) {
                isDraggingMouse = true;
                originPoint = Optional.of(e.getPoint());
                endingPoint = Optional.of(e.getPoint());
                repaint();
            }
        }

        @Override
        public void mouseReleased(final MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e) && isDraggingMouse) {
                endingPoint = Optional.of(e.getPoint());
                if (status == ViewStatus.MOVING && originPoint.isPresent() && endingPoint.isPresent()) {
                    if (currentEnv.getDimensions() == 2) {
                        final Simulation<T> engine = currentEnv.getSimulation();
                        if (engine != null) {
                            final Position envEnding = wormhole.getEnvPoint(endingPoint.get());
                            final Position envOrigin = wormhole.getEnvPoint(originPoint.get());
                            for (final Node<T> n : selectedNodes) {
                                final Position p = currentEnv.getPosition(n);
                                final double finalX = p.getCoordinate(0)
                                        + (envEnding.getCoordinate(0) - envOrigin.getCoordinate(0));
                                final double finalY = p.getCoordinate(1)
                                        + (envEnding.getCoordinate(1) - envOrigin.getCoordinate(1));
                                final Position finalPos = PointAdapter.from(finalX, finalY).toPosition();
                                engine.schedule(() -> {
                                    currentEnv.moveNodeToPosition(n, finalPos);
                                    update(currentEnv, engine.getTime());
                                });
                            }
                        } else {
                            // TODO: display proper error message
                            L.warn("Can not handle node movement on a finished simulation.");
                        }
                    } else {
                        throw new IllegalStateException("Unable to move nodes: unsupported environment dimension.");
                    }
                    selectedNodes.clear();
                    resetStatus();
                }
                isDraggingMouse = false;
                originPoint = Optional.empty();
                endingPoint = Optional.empty();
                repaint();
            }
        }

        @Override
        public void mouseWheelMoved(final MouseWheelEvent e) {
            if (wormhole != null && zoomManager != null) {
                zoomManager.dec(e.getWheelRotation());
                wormhole.zoomOnPoint(e.getPoint(), zoomManager.getZoom());
                updateMouse(e);
            }
        }

        private void updateMouse(final MouseEvent e) {
            setDist(e.getX(), e.getY());
            if (isCloserNodeMarked()) {
                repaint();
            }
        }

    }

    private static JFrame makeFrame(final String title, final JPanel content) {
        final JFrame frame = new JFrame(title);
        frame.getContentPane().add(content);
        frame.setLocationByPlatform(true);
        frame.pack();
        frame.setVisible(true);
        return frame;
    }

    private void bindKey(final int key, final Runnable fun) {
        final Object binder = "Key: " + key;
        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(key, 0), binder);
        getActionMap().put(binder, new AbstractAction() {
            private static final long serialVersionUID = 7927420406960259675L;

            @Override
            public void actionPerformed(final ActionEvent e) {
                fun.run();
            }
        });
    }

    private static boolean isInsideRectangle(final Point viewPoint, final int rx, final int ry, final int width,
            final int height) {
        final double x = viewPoint.getX();
        final double y = viewPoint.getY();
        return x >= rx && x <= rx + width && y >= ry && y <= ry + height;
    }

    private enum ViewStatus {

        VIEW_ONLY,

        MARK_CLOSER,

        SELECTING,

        MOVING,

        CLONING,

        DELETING,

        MOLECULING;
    }
}