[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.
This commit is contained in:
Enrico Joerns 2014-04-10 17:23:28 +02:00
parent a7dbf46b8e
commit 22ac769781
4 changed files with 298 additions and 173 deletions

View file

@ -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<Mote> 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 */

View file

@ -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<Mote> 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<Mote> 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<Class<? extends VisualizerSkin>> 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<? extends SimulationMenuAction> menuActionClass: simulationMenuActions) {
for (Class<? extends SimulationMenuAction> 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<Mote, double[]> 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<Mote> 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<Mote> getSelectedMotes() {
return selectedMotes;
}
public Collection<Element> 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);
}
}
}

View file

@ -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<Mote> 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();

View file

@ -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<Mote> 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;
}