From 22ac769781c454c085457267e3648a8ebb1b236d Mon Sep 17 00:00:00 2001 From: Enrico Joerns Date: Thu, 10 Apr 2014 17:23:28 +0200 Subject: [PATCH] [cooja] plugins/Visualizer: Multi-mote selection functionality This adds multi-mote selection capabilities to the visualizer plugin: - [Ctrl + Mouse Drag]: Rectangular selection of multiple motes - [Ctrl + Mouse Click]: Add/Remove motes from current selection - Mouse Drag on any selected Mote: Move all currently selected motes Note: This changes previous behaviour of using Ctrl key. --- .../org/contikios/mrm/MRMVisualizerSkin.java | 16 +- .../contikios/cooja/plugins/Visualizer.java | 425 +++++++++++------- .../plugins/skins/DGRMVisualizerSkin.java | 15 +- .../plugins/skins/UDGMVisualizerSkin.java | 15 +- 4 files changed, 298 insertions(+), 173 deletions(-) diff --git a/tools/cooja/apps/mrm/java/org/contikios/mrm/MRMVisualizerSkin.java b/tools/cooja/apps/mrm/java/org/contikios/mrm/MRMVisualizerSkin.java index d18f1c66b..22714dc6d 100644 --- a/tools/cooja/apps/mrm/java/org/contikios/mrm/MRMVisualizerSkin.java +++ b/tools/cooja/apps/mrm/java/org/contikios/mrm/MRMVisualizerSkin.java @@ -33,6 +33,7 @@ import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; +import java.util.Set; import org.apache.log4j.Logger; @@ -72,20 +73,23 @@ public class MRMVisualizerSkin implements VisualizerSkin { } public Color[] getColorOf(Mote mote) { - Mote selectedMote = visualizer.getSelectedMote(); - if (mote == selectedMote) { + if (visualizer.getSelectedMotes().contains(mote)) { return new Color[] { Color.CYAN }; } return null; } public void paintBeforeMotes(Graphics g) { - final Mote selectedMote = visualizer.getSelectedMote(); - if (simulation == null - || selectedMote == null - || selectedMote.getInterfaces().getRadio() == null) { + Set selectedMotes = visualizer.getSelectedMotes(); + if (simulation == null || selectedMotes == null || selectedMotes.isEmpty()) { return; } + + final Mote selectedMote = visualizer.getSelectedMotes().iterator().next(); + if (selectedMote.getInterfaces().getRadio() == null) { + return; + } + final Position sPos = selectedMote.getInterfaces().getPosition(); /* Paint transmission and interference range for selected mote */ diff --git a/tools/cooja/java/org/contikios/cooja/plugins/Visualizer.java b/tools/cooja/java/org/contikios/cooja/plugins/Visualizer.java index 8926e9af7..14ef496e2 100644 --- a/tools/cooja/java/org/contikios/cooja/plugins/Visualizer.java +++ b/tools/cooja/java/org/contikios/cooja/plugins/Visualizer.java @@ -30,10 +30,12 @@ package org.contikios.cooja.plugins; +import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; +import java.awt.Event; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; @@ -55,7 +57,7 @@ import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseMotionListener; +import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; @@ -64,9 +66,14 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Observable; import java.util.Observer; +import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; @@ -74,7 +81,6 @@ import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSeparator; @@ -128,6 +134,7 @@ import org.contikios.cooja.plugins.skins.UDGMVisualizerSkin; * @see #registerVisualizerSkin(Class) * @see UDGMVisualizerSkin * @author Fredrik Osterlind + * @author Enrico Jorns */ @ClassDescription("Network") @PluginType(PluginType.SIM_STANDARD_PLUGIN) @@ -149,20 +156,34 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { private AffineTransform viewportTransform; public int resetViewport = 0; - /* Actions: move motes, pan view, and zoom view */ - private boolean panning = false; - private Position panningPosition = null; /* Panning start position */ - private boolean zooming = false; - private double zoomStart = 0; - private Position zoomingPosition = null; /* Zooming center position */ - private Point zoomingPixel = null; /* Zooming center pixel */ - private boolean moving = false; - private Point mouseDownPixel = null; /* Records position of mouse down to differentiate a click from a move */ - private Mote movedMote = null; - public Mote clickedMote = null; + private static final int SELECT_MASK = Event.CTRL_MASK; + private static final int MOVE_MASK = Event.SHIFT_MASK; + + enum MotesActionState { + + UNKNWON, + SELECT_PRESS, + DEFAULT_PRESS, + PAN_PRESS, + PANNING, + MOVING, + // rectangular select + SELECTING + } + + /* All selected motes */ + public Set selectedMotes = new HashSet<>(); + /* Mote that was under curser while mouse press */ + Mote cursorMote; + + MotesActionState mouseActionState = MotesActionState.UNKNWON; + /* Position where mouse button was pressed */ + Position pressedPos; + + private Set movedMotes = null; private long moveStartTime = -1; - private boolean moveConfirm; - private Cursor moveCursor = new Cursor(Cursor.MOVE_CURSOR); + private static final Cursor MOVE_CURSOR = new Cursor(Cursor.MOVE_CURSOR); + private Selection selection; /* Visualizers */ private static ArrayList> visualizerSkins = @@ -277,6 +298,7 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { }); zoomMenu.add(resetViewportItem); + selection = new Selection(); /* Main canvas */ canvas = new JPanel() { private static final long serialVersionUID = 1L; @@ -296,6 +318,7 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { for (VisualizerSkin skin: currentSkins) { skin.paintAfterMotes(g); } + selection.drawSelection(g); } }; canvas.setBackground(Color.WHITE); @@ -378,18 +401,16 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { }); /* Popup menu */ - canvas.addMouseMotionListener(new MouseMotionListener() { - public void mouseMoved(MouseEvent e) { - handleMouseMove(e, false); - } + canvas.addMouseMotionListener(new MouseMotionAdapter() { + @Override public void mouseDragged(MouseEvent e) { - handleMouseMove(e, false); + handleMouseDrag(e, false); } }); canvas.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { - handlePopupRequest(e.getPoint().x, e.getPoint().y); + handlePopupRequest(e.getPoint()); return; } @@ -399,19 +420,13 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { } public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { - handlePopupRequest(e.getPoint().x, e.getPoint().y); + handlePopupRequest(e.getPoint()); return; } - handleMouseMove(e, true); - } - public void mouseClicked(MouseEvent e) { - if (e.isPopupTrigger()) { - handlePopupRequest(e.getPoint().x, e.getPoint().y); + if (SwingUtilities.isLeftMouseButton(e)) { + handleMouseRelease(e); } - - handleMouseMove(e, true); - repaint(); } }); canvas.addMouseWheelListener(new MouseWheelListener() { @@ -645,11 +660,11 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { visualizerSkins.remove(skin); } - private void handlePopupRequest(final int x, final int y) { + private void handlePopupRequest(Point point) { JPopupMenu menu = new JPopupMenu(); /* Mote specific actions */ - final Mote[] motes = findMotesAtPosition(x, y); + final Mote[] motes = findMotesAtPosition(point.x, point.y); if (motes != null && motes.length > 0) { menu.add(new JSeparator()); @@ -679,7 +694,7 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { /* Simulation specific actions */ menu.add(new JSeparator()); - for (Class menuActionClass: simulationMenuActions) { + for (Class menuActionClass : simulationMenuActions) { try { final SimulationMenuAction menuAction = menuActionClass.newInstance(); if (menuAction.isEnabled(this, simulation)) { @@ -709,8 +724,8 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { /* Show menu */ menu.setLocation(new Point( - canvas.getLocationOnScreen().x + x, - canvas.getLocationOnScreen().y + y)); + canvas.getLocationOnScreen().x + point.x, + canvas.getLocationOnScreen().y + point.y)); menu.setInvoker(canvas); menu.setVisible(true); } @@ -826,43 +841,169 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { private void handleMousePress(MouseEvent mouseEvent) { int x = mouseEvent.getX(); int y = mouseEvent.getY(); - clickedMote = null; - if (mouseEvent.isControlDown()) { - /* Zoom */ - zooming = true; - zoomingPixel = new Point(x, y); - zoomingPosition = transformPixelToPosition(zoomingPixel); - zoomStart = viewportTransform.getScaleX(); - return; + pressedPos = transformPixelToPosition(mouseEvent.getPoint()); + + // this is the state we have from pressing button + final Mote[] foundMotes = findMotesAtPosition(x, y); + if (foundMotes == null) { + cursorMote = null; + } + else { + // select top mote + cursorMote = foundMotes[foundMotes.length - 1]; } - final Mote[] motes = findMotesAtPosition(x, y); - if (mouseEvent.isShiftDown() || - (!mouseEvent.isAltDown() && (motes == null || motes.length == 0))) { - /* No motes clicked or shift pressed: We should pan */ - panning = true; - panningPosition = transformPixelToPosition(x, y); - return; - } + int modifiers = mouseEvent.getModifiers(); - if (motes != null && motes.length > 0) { - /* One of the clicked motes should be moved */ - mouseDownPixel = new Point(x, y); - clickedMote = motes[0]; - beginMoveRequest(motes[0], false, false); + /* translate input */ + if ((modifiers & SELECT_MASK) != 0) { + mouseActionState = MotesActionState.SELECT_PRESS; + } + else if ((modifiers & MOVE_MASK) != 0) { + // only move viewport + mouseActionState = MotesActionState.PAN_PRESS; + } + else { + if (foundMotes == null) { + // move viewport + selectedMotes.clear(); + } + else { + // if this mote was not selected before, assume a new selection + if (!selectedMotes.contains(cursorMote)) { + selectedMotes.clear(); + selectedMotes.add(cursorMote); + } + } + mouseActionState = MotesActionState.DEFAULT_PRESS; + } + repaint(); + } + + Map moveStartPositions = new HashMap<>(); + + private void handleMouseDrag(MouseEvent e, boolean stop) { + Position currPos = transformPixelToPosition(e.getPoint()); + + switch (mouseActionState) { + case DEFAULT_PRESS: + if (cursorMote == null) { + mouseActionState = MotesActionState.PANNING; + } + else { + mouseActionState = MotesActionState.MOVING; + // save start position + for (Mote m : selectedMotes) { + Position pos = m.getInterfaces().getPosition(); + moveStartPositions.put(m, new double[]{ + pos.getXCoordinate(), + pos.getYCoordinate(), + pos.getZCoordinate()}); + } + } + break; + case MOVING: + canvas.setCursor(MOVE_CURSOR); + for (Mote moveMote : selectedMotes) { + moveMote.getInterfaces().getPosition().setCoordinates( + moveStartPositions.get(moveMote)[0] + + (currPos.getXCoordinate() - pressedPos.getXCoordinate()), + moveStartPositions.get(moveMote)[1] + + (currPos.getYCoordinate() - pressedPos.getYCoordinate()), + moveStartPositions.get(moveMote)[2] + ); + repaint(); + } + break; + case PAN_PRESS: + mouseActionState = MotesActionState.PANNING; + break; + case PANNING: + /* The current mouse position should correspond to where panning started */ + viewportTransform.translate( + currPos.getXCoordinate() - pressedPos.getXCoordinate(), + currPos.getYCoordinate() - pressedPos.getYCoordinate() + ); + repaint(); + break; + case SELECT_PRESS: + mouseActionState = MotesActionState.SELECTING; + selection.setEnabled(true); + break; + case SELECTING: + int pressedX = transformToPixelX(pressedPos.getXCoordinate()); + int pressedY = transformToPixelY(pressedPos.getYCoordinate()); + int currX = transformToPixelX(currPos.getXCoordinate()); + int currY = transformToPixelY(currPos.getYCoordinate()); + int startX = pressedX < currX ? pressedX : currX; + int startY = pressedY < currY ? pressedY : currY; + int width = Math.abs(pressedX - currX); + int height = Math.abs(pressedY - currY); + + selection.setSelection(startX, startY, width, height); + selectedMotes.clear(); + selectedMotes.addAll(Arrays.asList(findMotesInRange(startX, startY, width, height))); + + repaint(); + break; } } - private void beginMoveRequest(Mote moteToMove, boolean withTiming, boolean confirm) { + private void handleMouseRelease(MouseEvent mouseEvent) { + + switch (mouseActionState) { + case PAN_PRESS: + // ignore + break; + case SELECT_PRESS: + if (cursorMote == null) { + /* Click on free canvas deselects all mote */ + selectedMotes.clear(); + } + else { + /* toggle selection for mote */ + if (selectedMotes.contains(cursorMote)) { + selectedMotes.remove(cursorMote); + } + else { + selectedMotes.add(cursorMote); + } + } + break; + case DEFAULT_PRESS: + if (cursorMote == null) { + /* Click on free canvas deselects all mote */ + selectedMotes.clear(); + } + else { + /* Click on mote selects single mote */ + selectedMotes.clear(); + selectedMotes.add(cursorMote); + } + break; + case MOVING: + /* Release stops moving */ + canvas.setCursor(Cursor.getDefaultCursor()); + break; + case SELECTING: + /* Release stops moving */ + selection.setEnabled(false); + repaint(); + break; + } + repaint(); + } + + private void beginMoveRequest(Mote motesToMove, boolean withTiming, boolean confirm) { if (withTiming) { moveStartTime = System.currentTimeMillis(); } else { moveStartTime = -1; } - moving = true; - moveConfirm = confirm; - movedMote = moteToMove; + mouseActionState = MotesActionState.DEFAULT_PRESS; + selectedMotes.clear(); + selectedMotes.add(motesToMove); repaint(); } @@ -888,107 +1029,37 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { repaint(); } - private void handleMouseMove(MouseEvent e, boolean stop) { - int x = e.getX(); - int y = e.getY(); - - /* Panning */ - if (panning) { - if (panningPosition == null || stop) { - panning = false; - return; + /** + * Returns all motes in rectangular range + * + * @param startX + * @param startY + * @param width + * @param height + * @return All motes in range + */ + public Mote[] findMotesInRange(int startX, int startY, int width, int height) { + List motes = new LinkedList<>(); + for (Mote m : simulation.getMotes()) { + Position pos = m.getInterfaces().getPosition(); + int moteX = transformToPixelX(pos.getXCoordinate()); + int moteY = transformToPixelY(pos.getYCoordinate()); + if (moteX > startX && moteX < startX + width + && moteY > startY && moteY < startY + height) { + motes.add(m); } - - /* The current mouse position should correspond to where panning started */ - Position moved = transformPixelToPosition(x,y); - viewportTransform.translate( - moved.getXCoordinate() - panningPosition.getXCoordinate(), - moved.getYCoordinate() - panningPosition.getYCoordinate() - ); - repaint(); - return; - } - - /* Zooming */ - if (zooming) { - if (zoomingPosition == null || zoomingPixel == null || stop) { - zooming = false; - return; - } - - /* The zooming start pixel should correspond to the zooming center position */ - /* The current mouse position should correspond to where panning started */ - double zoomFactor = 1.0 + Math.abs((double) zoomingPixel.y - y)/100.0; - double newZoom = (zoomingPixel.y - y)>0?zoomStart*zoomFactor: zoomStart/zoomFactor; - if (newZoom < 0.00001) { - newZoom = 0.00001; - } - viewportTransform.setToScale( - newZoom, - newZoom - ); - Position moved = transformPixelToPosition(zoomingPixel); - viewportTransform.translate( - moved.getXCoordinate() - zoomingPosition.getXCoordinate(), - moved.getYCoordinate() - zoomingPosition.getYCoordinate() - ); - repaint(); - return; - } - - /* Moving */ - if (moving) { - if(x != mouseDownPixel.x || y != mouseDownPixel.y) { - Position newPos = transformPixelToPosition(x, y); - - if (!stop) { - canvas.setCursor(moveCursor); - movedMote.getInterfaces().getPosition().setCoordinates( - newPos.getXCoordinate(), - newPos.getYCoordinate(), - movedMote.getInterfaces().getPosition().getZCoordinate() - ); - repaint(); - return; - } - /* Restore cursor */ - canvas.setCursor(Cursor.getDefaultCursor()); - - - /* Move mote */ - if (moveStartTime < 0 || System.currentTimeMillis() - moveStartTime > 300) { - if (moveConfirm) { - String options[] = {"Yes", "Cancel"}; - int returnValue = JOptionPane.showOptionDialog(Visualizer.this, - "Move mote to" + - "\nX=" + newPos.getXCoordinate() + - "\nY=" + newPos.getYCoordinate() + - "\nZ=" + movedMote.getInterfaces().getPosition().getZCoordinate(), - "Move mote?", - JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, - null, options, options[0]); - moving = returnValue == JOptionPane.YES_OPTION; - } - if (moving) { - movedMote.getInterfaces().getPosition().setCoordinates( - newPos.getXCoordinate(), - newPos.getYCoordinate(), - movedMote.getInterfaces().getPosition().getZCoordinate() - ); - repaint(); - } - } - } - - moving = false; - movedMote = null; - repaint(); } + Mote[] motesArr = new Mote[motes.size()]; + return motes.toArray(motesArr); } /** * Returns all motes at given position. * + * If multiple motes were found on at a position the motes are returned + * in the order they are painted on screen. + * First mote in array is the bottom mote, last mote is the top mote. + * * @param clickedX * X coordinate * @param clickedY @@ -1066,7 +1137,7 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { int x = pixelCoord.x; int y = pixelCoord.y; - if (mote == movedMote) { + if (mote == movedMotes) { g.setColor(MOVE_COLOR); g.fillOval(x - MOTE_RADIUS, y - MOTE_RADIUS, 2 * MOTE_RADIUS, 2 * MOTE_RADIUS); @@ -1292,8 +1363,8 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { /** * @return Selected mote */ - public Mote getSelectedMote() { - return clickedMote; + public Set getSelectedMotes() { + return selectedMotes; } public Collection getConfigXML() { @@ -1585,4 +1656,44 @@ public class Visualizer extends VisPlugin implements HasQuickHelp { "Multiple views can be active at the same time. " + "Use the View menu to select views. "; }; + + private class Selection { + + private int x; + private int y; + private int width; + private int height; + private boolean enable; + + public void setSelection(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public void setEnabled(boolean enable) { + this.enable = enable; + } + + public void drawSelection(Graphics g) { + /* only draw if enabled */ + if (!enable) { + return; + } + Graphics2D g2d = (Graphics2D) g; + g2d.setColor(new Color(64, 64, 64, 10)); + g2d.fillRect(x, y, width, height); + + BasicStroke dashed + = new BasicStroke(1.0f, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, + 10.0f, new float[]{5.0f}, 0.0f); + g2d.setColor(Color.BLACK); + g2d.setStroke(dashed); + g2d.drawRect(x, y, width, height); + } + } + } diff --git a/tools/cooja/java/org/contikios/cooja/plugins/skins/DGRMVisualizerSkin.java b/tools/cooja/java/org/contikios/cooja/plugins/skins/DGRMVisualizerSkin.java index 3a3176a91..ccd3481d1 100644 --- a/tools/cooja/java/org/contikios/cooja/plugins/skins/DGRMVisualizerSkin.java +++ b/tools/cooja/java/org/contikios/cooja/plugins/skins/DGRMVisualizerSkin.java @@ -34,6 +34,7 @@ import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; +import java.util.Set; import org.apache.log4j.Logger; @@ -74,21 +75,25 @@ public class DGRMVisualizerSkin implements VisualizerSkin { } public Color[] getColorOf(Mote mote) { - Mote selectedMote = visualizer.getSelectedMote(); - if (mote == selectedMote) { + if (visualizer.getSelectedMotes().contains(mote)) { return new Color[] { Color.CYAN }; } return null; } public void paintBeforeMotes(Graphics g) { - Mote selectedMote = visualizer.getSelectedMote(); + Set selectedMotes = visualizer.getSelectedMotes(); if (simulation == null - || selectedMote == null - || selectedMote.getInterfaces().getRadio() == null) { + || selectedMotes == null + || selectedMotes.isEmpty()) { return; } + final Mote selectedMote = visualizer.getSelectedMotes().iterator().next(); + if (selectedMote.getInterfaces().getRadio() == null) { + return; + } + /* Paint transmission and interference range for selected mote */ Position motePos = selectedMote.getInterfaces().getPosition(); diff --git a/tools/cooja/java/org/contikios/cooja/plugins/skins/UDGMVisualizerSkin.java b/tools/cooja/java/org/contikios/cooja/plugins/skins/UDGMVisualizerSkin.java index 26f66e3e7..bcffa0eb9 100644 --- a/tools/cooja/java/org/contikios/cooja/plugins/skins/UDGMVisualizerSkin.java +++ b/tools/cooja/java/org/contikios/cooja/plugins/skins/UDGMVisualizerSkin.java @@ -36,6 +36,7 @@ import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.beans.PropertyVetoException; +import java.util.Set; import javax.swing.BorderFactory; import javax.swing.Box; @@ -259,8 +260,7 @@ public class UDGMVisualizerSkin implements VisualizerSkin { @Override public Color[] getColorOf(Mote mote) { - Mote selectedMote = visualizer.getSelectedMote(); - if (mote == selectedMote) { + if (visualizer.getSelectedMotes().contains(mote)) { return new Color[] { Color.CYAN }; } return null; @@ -268,10 +268,15 @@ public class UDGMVisualizerSkin implements VisualizerSkin { @Override public void paintBeforeMotes(Graphics g) { - Mote selectedMote = visualizer.getSelectedMote(); + Set selectedMotes = visualizer.getSelectedMotes(); if (simulation == null - || selectedMote == null - || selectedMote.getInterfaces().getRadio() == null) { + || selectedMotes == null + || selectedMotes.isEmpty()) { + return; + } + + final Mote selectedMote = visualizer.getSelectedMotes().iterator().next(); + if (selectedMote.getInterfaces().getRadio() == null) { return; }