From febb07a71b8371bdc3951ae445ee87264be257c3 Mon Sep 17 00:00:00 2001 From: nifi Date: Sun, 10 Oct 2010 22:39:09 +0000 Subject: [PATCH] Added simple spring layout that attracts connected nodes in node visualizer --- .../sics/contiki/collect/CollectServer.java | 35 +- .../src/se/sics/contiki/collect/Node.java | 26 +- .../se/sics/contiki/collect/gui/MapPanel.java | 590 +++++++++++------- 3 files changed, 386 insertions(+), 265 deletions(-) diff --git a/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java b/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java index e65b22b59..4ce44b647 100644 --- a/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java +++ b/examples/sky-shell/src/se/sics/contiki/collect/CollectServer.java @@ -26,7 +26,7 @@ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * - * $Id: CollectServer.java,v 1.24 2010/10/07 21:13:00 nifi Exp $ + * $Id: CollectServer.java,v 1.25 2010/10/10 22:39:09 nifi Exp $ * * ----------------------------------------------------------------- * @@ -34,8 +34,8 @@ * * Authors : Joakim Eriksson, Niclas Finne * Created : 3 jul 2008 - * Updated : $Date: 2010/10/07 21:13:00 $ - * $Revision: 1.24 $ + * Updated : $Date: 2010/10/10 22:39:09 $ + * $Revision: 1.25 $ */ package se.sics.contiki.collect; @@ -229,7 +229,7 @@ public class CollectServer implements SerialConnectionListener { categoryTable.put(MAIN, mainPanel); serialConsole = new SerialConsole(this, MAIN); - mapPanel = new MapPanel(this, MAIN); + mapPanel = new MapPanel(this, "Sensor Map", MAIN, true); String image = getConfig("collect.mapimage"); if (image != null) { mapPanel.setMapBackground(image); @@ -237,6 +237,7 @@ public class CollectServer implements SerialConnectionListener { final int defaultMaxItemCount = 250; visualizers = new Visualizer[] { mapPanel, + new MapPanel(this, "Network Graph", MAIN, false), new BarChartPanel(this, SENSORS, "Average Temperature", "Temperature", "Nodes", "Celsius", new String[] { "Celsius" }) { { @@ -867,7 +868,6 @@ public class CollectServer implements SerialConnectionListener { if (node == null) { node = new Node(nodeID); nodeTable.put(nodeID, node); - updateNodeLocation(node); synchronized (this) { nodeCache = null; @@ -940,31 +940,6 @@ public class CollectServer implements SerialConnectionListener { // Node location handling // ------------------------------------------------------------------- - public boolean updateNodeLocation(Node node) { - String id = node.getID(); - if (node.hasLocation()) { - String location = "" + node.getX() + ',' + node.getY(); - if (!location.equals(configTable.get(id))) { - configTable.put(id, location); - } - return false; - } - - String location = configTable.getProperty(id); - if (location != null) { - try { - String[] pos = location.split(","); - node.setLocation(Integer.parseInt(pos[0].trim()), - Integer.parseInt(pos[1].trim())); - return true; - } catch (Exception e) { - System.err.println("could not parse node location: " + location); - e.printStackTrace(); - } - } - return false; - } - private boolean loadConfig(Properties properties, String configFile) { try { BufferedInputStream input = diff --git a/examples/sky-shell/src/se/sics/contiki/collect/Node.java b/examples/sky-shell/src/se/sics/contiki/collect/Node.java index cfb403378..56bd21a95 100644 --- a/examples/sky-shell/src/se/sics/contiki/collect/Node.java +++ b/examples/sky-shell/src/se/sics/contiki/collect/Node.java @@ -26,7 +26,7 @@ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * - * $Id: Node.java,v 1.7 2010/10/07 21:13:00 nifi Exp $ + * $Id: Node.java,v 1.8 2010/10/10 22:39:09 nifi Exp $ * * ----------------------------------------------------------------- * @@ -34,8 +34,8 @@ * * Authors : Joakim Eriksson, Niclas Finne * Created : 3 jul 2008 - * Updated : $Date: 2010/10/07 21:13:00 $ - * $Revision: 1.7 $ + * Updated : $Date: 2010/10/10 22:39:09 $ + * $Revision: 1.8 $ */ package se.sics.contiki.collect; @@ -56,9 +56,6 @@ public class Node implements Comparable { private final String id; private final String name; - public int x = -1; - public int y = -1; - private Hashtable objectTable; private long lastActive; @@ -77,23 +74,6 @@ public class Node implements Comparable { return name; } - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public void setLocation(int x, int y) { - this.x = x; - this.y = y; - } - - public boolean hasLocation() { - return x >= 0 && y >= 0; - } - public long getLastActive() { return lastActive; } diff --git a/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java b/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java index 3768b8a4c..a6db18107 100644 --- a/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java +++ b/examples/sky-shell/src/se/sics/contiki/collect/gui/MapPanel.java @@ -26,7 +26,7 @@ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * - * $Id: MapPanel.java,v 1.3 2010/09/15 16:15:10 nifi Exp $ + * $Id: MapPanel.java,v 1.4 2010/10/10 22:39:09 nifi Exp $ * * ----------------------------------------------------------------- * @@ -34,12 +34,11 @@ * * Authors : Joakim Eriksson, Niclas Finne * Created : 3 jul 2008 - * Updated : $Date: 2010/09/15 16:15:10 $ - * $Revision: 1.3 $ + * Updated : $Date: 2010/10/10 22:39:09 $ + * $Revision: 1.4 $ */ package se.sics.contiki.collect.gui; -import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; @@ -47,22 +46,35 @@ import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.GridLayout; +import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; -import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.Hashtable; +import java.util.Properties; import java.util.logging.Logger; + import javax.swing.ImageIcon; +import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JSlider; import javax.swing.Timer; +import javax.swing.border.LineBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.plaf.basic.BasicGraphicsUtils; + import se.sics.contiki.collect.CollectServer; +import se.sics.contiki.collect.Configurable; import se.sics.contiki.collect.Link; import se.sics.contiki.collect.Node; import se.sics.contiki.collect.SensorData; @@ -71,48 +83,40 @@ import se.sics.contiki.collect.Visualizer; /** * */ -public class MapPanel extends JPanel implements Visualizer, ActionListener, MouseListener, MouseMotionListener { +public class MapPanel extends JPanel implements Configurable, Visualizer, ActionListener, MouseListener, MouseMotionListener { private static final long serialVersionUID = -8256619482599309425L; private static final Logger log = Logger.getLogger(MapPanel.class.getName()); - private static final boolean VISUAL_DRAG = false; + private static final boolean VISUAL_DRAG = true; - private static final int FADE_COUNT = 20; - private static final int AGE_COUNT = 200; - - private static final Color[] OTHER_COLOR = new Color[FADE_COUNT]; - private static final Color[] LINK_COLOR = new Color[AGE_COUNT]; - - static { - for (int i = 0; i < FADE_COUNT; i++) { - OTHER_COLOR[i] = new Color(0xe0,0xe0,0x00,0xFF - - ((i * 255) / (FADE_COUNT - 1))); - } - - for (int i = 0, n = AGE_COUNT; i < n; i++) { - LINK_COLOR[i] = new Color(0x40 + i / 2, 0x40 + i / 2, 0xf0, 0xff - i); - } - } + private static final Color LINK_COLOR = new Color(0x40, 0x40, 0xf0, 0xff); private static final int delta = 7; - public static final int SHOW_BLINK = 40; - public static final int TOTAL_SHOW = 600; + private final CollectServer server; + private final String category; + private final boolean isMap; + private String title; - private Timer timer = new Timer(400, this); - private boolean hasPendingEvents = false; - private int ticker = 0; + private Timer timer; private JPopupMenu popupMenu; -// private MapNode popupNode; - private JMenuItem hideItem; + private JCheckBoxMenuItem layoutItem; + private JCheckBoxMenuItem lockedItem; + private JMenuItem shakeItem; +// private JCheckBoxMenuItem dragItem; + private JCheckBoxMenuItem backgroundItem; + private JCheckBoxMenuItem showNetworkItem; + private JCheckBoxMenuItem configItem; private JMenuItem resetNetworkItem; + private MapNode popupNode; private Hashtable nodeTable = new Hashtable(); - private MapNode[] nodeList; + private MapNode[] nodeList = new MapNode[0]; + private boolean updateNodeList; private MapNode selectedNode; private ArrayList selectedMapNodes = new ArrayList(); @@ -122,36 +126,120 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous private ImageIcon mapImage; private String mapName; + private boolean showBackground; - private final CollectServer server; - private final String category; + private int layoutRepel = 100; + private int layoutAttract = 50; + private int layoutGravity = 1; + private boolean isLayoutActive = true; private boolean hideNetwork = false; - public MapPanel(CollectServer server, String category) { - super(new BorderLayout()); + protected JPanel configPanel; + + public MapPanel(CollectServer server, String title, String category, boolean isMap) { + super(null); this.server = server; + this.title = title; this.category = category; + this.isMap = isMap; setPreferredSize(new Dimension(300, 200)); popupMenu = new JPopupMenu(getTitle()); -// popupMenu.addSeparator(); - hideItem = createMenuItem(popupMenu, "Hide Network"); - resetNetworkItem = createMenuItem(popupMenu, "Reset Network"); + if (!isMap) { + layoutItem = createCheckBoxMenuItem(popupMenu, "Update Layout", isLayoutActive); + popupMenu.add(layoutItem); + lockedItem = createCheckBoxMenuItem(popupMenu, "Fixed Node Position", false); + shakeItem = createMenuItem(popupMenu, "Shake Nodes"); + popupMenu.addSeparator(); + } + + showNetworkItem = createCheckBoxMenuItem(popupMenu, "Show Network Info", true); + resetNetworkItem = createMenuItem(popupMenu, "Reset Network"); + popupMenu.addSeparator(); + if (isMap) { + backgroundItem = createCheckBoxMenuItem(popupMenu, "Show Background", false); + backgroundItem.setEnabled(false); + } else { + configItem = createCheckBoxMenuItem(popupMenu, "Show Layout Settings", false); + } + +// popupMenu.addSeparator(); +// dragItem = new JCheckBoxMenuItem("Visible Drag", true); +// popupMenu.add(dragItem); + + setBackground(Color.white); addMouseListener(this); addMouseMotionListener(this); - setBackground(Color.white); + + if (!isMap) { + timer = new Timer(100, this); + + configPanel = new JPanel(new GridLayout(0, 1)); + configPanel.setBorder(LineBorder.createBlackLineBorder()); + + JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, 1000, 1000 - layoutAttract); + slider.setBorder(new TitledBorder("Attract Factor: " + (1000 - layoutAttract))); + slider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSlider slider = (JSlider)e.getSource(); + layoutAttract = 1000 - slider.getValue(); + ((TitledBorder)slider.getBorder()).setTitle("Attract Factor: " + slider.getValue()); + } + }); + configPanel.add(slider); + + slider = new JSlider(JSlider.HORIZONTAL, 0, 1000, layoutRepel); + slider.setBorder(new TitledBorder("Repel Range: " + layoutRepel)); + slider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSlider slider = (JSlider)e.getSource(); + layoutRepel = slider.getValue(); + ((TitledBorder)slider.getBorder()).setTitle("Repel Range: " + layoutRepel); + } + }); + configPanel.add(slider); + + slider = new JSlider(JSlider.HORIZONTAL, 0, 100, layoutGravity); + slider.setBorder(new TitledBorder("Gravity: " + layoutGravity)); + slider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSlider slider = (JSlider)e.getSource(); + layoutGravity = slider.getValue(); + ((TitledBorder)slider.getBorder()).setTitle("Gravity: " + layoutGravity); + } + }); + configPanel.add(slider); + + add(configPanel); + configPanel.setVisible(false); + + addComponentListener(new ComponentAdapter() { + public void componentResized(ComponentEvent ev) { + if (configPanel.isVisible()) { + updateConfigLayout(); + } + } + }); + } } public String getMapBackground() { - return mapName; + return isMap ? mapName : null; } public boolean setMapBackground(String image) { + if (!isMap) { + return false; + } if (image == null) { mapImage = null; mapName = null; + backgroundItem.setEnabled(false); + backgroundItem.setSelected(false); + showBackground = false; + repaint(); return true; } @@ -163,10 +251,20 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous mapImage = ii; mapName = image; setPreferredSize(new Dimension(ii.getIconWidth(), ii.getIconHeight())); + showBackground = true; + backgroundItem.setEnabled(true); + backgroundItem.setSelected(true); repaint(); return true; } + private JCheckBoxMenuItem createCheckBoxMenuItem(JPopupMenu menu, String title, boolean isSelected) { + JCheckBoxMenuItem item = new JCheckBoxMenuItem(title, isSelected); + item.addActionListener(this); + menu.add(item); + return item; + } + private JMenuItem createMenuItem(JPopupMenu menu, String title) { JMenuItem item = new JMenuItem(title); item.addActionListener(this); @@ -177,9 +275,13 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous public void setVisible(boolean visible) { if (visible) { clear(); - timer.start(); + if (timer != null) { + timer.start(); + } } else { - timer.stop(); + if (timer != null) { + timer.stop(); + } } super.setVisible(visible); } @@ -187,7 +289,6 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous public void clear() { setCursor(Cursor.getDefaultCursor()); draggedNode = null; - hasPendingEvents = false; updateSelected(); } @@ -206,15 +307,40 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous } private MapNode addMapNode(Node nd) { - MapNode node = nodeTable.get(nd.getID()); + String id = nd.getID(); + MapNode node = nodeTable.get(id); if (node == null) { node = new MapNode(this, nd); - synchronized (this) { - nodeTable.put("" + nd.getID(), node); + node.y = 10 + (int) (Math.random() * (Math.max(100, getHeight()) - 20)); + node.x = 10 + (int) (Math.random() * (Math.max(100, getWidth()) - 30)); + + String location = server.getConfig(isMap ? id : ("collect.map." + id)); + if (location != null) { + try { + String[] pos = location.split(","); + node.x = Integer.parseInt(pos[0].trim()); + node.y = Integer.parseInt(pos[1].trim()); + node.hasFixedLocation = !isMap; + } catch (Exception e) { + System.err.println("could not parse node location: " + location); + e.printStackTrace(); + } + } + + nodeTable.put(id, node); + updateNodeList = true; + } + return node; + } + + private MapNode[] getNodeList() { + if (updateNodeList) { + synchronized (nodeTable) { + updateNodeList = false; nodeList = nodeTable.values().toArray(new MapNode[nodeTable.size()]); } } - return node; + return nodeList; } @@ -229,7 +355,7 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous @Override public String getTitle() { - return "Sensor Map"; + return title; } @Override @@ -286,7 +412,12 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous @Override public void clearNodeData() { - // Ignore + nodeTable.clear(); + updateNodeList = true; + nodesSelected(null); + if (isVisible()) { + repaint(); + } } @@ -297,80 +428,53 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous @Override protected void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; - int lx = 10; - long time = System.currentTimeMillis(); g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); - if (mapImage != null) { + if (showBackground && isMap) { mapImage.paintIcon(this, g, 0, 0); } - MapNode[] nodes = nodeList; - if (nodes != null) { - Line2D line = new Line2D.Double(); - for (int i = 0, m = nodes.length; i < m; i++) { - MapNode n = nodes[i]; - int x, y; - if (n.node.hasLocation()) { - x = n.node.getX(); - y = n.node.getY(); - } else { - x = lx; - y = 10; - lx += 30; - } - - if (!hideNetwork) { - FontMetrics fm = g.getFontMetrics(); - int fnHeight = fm.getHeight(); - int fnDescent = fm.getDescent(); - for (int j = 0, mu = n.node.getLinkCount(); j < mu; j++) { - Link link = n.node.getLink(j); - if (link.node.hasLocation()) { - long age = (time - link.getLastActive()) / 100; - int x2 = link.node.getX(); - int y2 = link.node.getY(); - if (n.isSelected) { - if (age > LINK_COLOR.length) { - age = 100; - } else { - age -= 50; - } - } - line.setLine(x, y, x2, y2); - if (age < LINK_COLOR.length) { - g.setColor(age < 0 ? LINK_COLOR[0] : LINK_COLOR[(int) age]); - } else { - g.setColor(LINK_COLOR[LINK_COLOR.length - 1]); - } - g2d.draw(line); -// g.setColor(Color.lightGray); - int xn1, xn2, yn1, yn2; - if (x <= x2) { - xn1 = x; xn2 = x2; - yn1 = y; yn2 = y2; - } else { - xn1 = x2; xn2 = x; - yn1 = y2; yn2 = y; - } - int dx = xn1 + (xn2 - xn1) / 2 + 4; - int dy = yn1 + (yn2 - yn1) / 2 - fnDescent; - if (yn2 < yn1) { - dy += fnHeight - fnDescent; - } - g.drawString("ETX:" + (((int)(link.getETX() * 100 + 0.5)) / 100.0), dx, dy); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + for (MapNode n : getNodeList()) { + if (!isMap || !hideNetwork) { + FontMetrics fm = g.getFontMetrics(); + int fnHeight = fm.getHeight(); + int fnDescent = fm.getDescent(); + g.setColor(LINK_COLOR); + for (int j = 0, mu = n.node.getLinkCount(); j < mu; j++) { + Link link = n.node.getLink(j); + MapNode linkNode = addMapNode(link.node); + int x2 = linkNode.x; + int y2 = linkNode.y; + g2d.drawLine(n.x, n.y, x2, y2); + if (!hideNetwork) { + int xn1, xn2, yn1, yn2; + if (n.x <= x2) { + xn1 = n.x; xn2 = x2; + yn1 = n.y; yn2 = y2; + } else { + xn1 = x2; xn2 = n.x; + yn1 = y2; yn2 = n.y; } + int dx = xn1 + (xn2 - xn1) / 2 + 4; + int dy = yn1 + (yn2 - yn1) / 2 - fnDescent; + if (yn2 < yn1) { + dy += fnHeight - fnDescent; + } + g.drawString("ETX:" + (((int)(link.getETX() * 100 + 0.5)) / 100.0), dx, dy); } } + } - n.paint(g, x, y); + n.paint(g, n.x, n.y); - g.setColor(Color.black); - if (n.isSelected) { - BasicGraphicsUtils.drawDashedRect(g, x - delta, y - delta, 2 * delta, 2 * delta); - } - if (selectedNode != null && selectedNode.message != null) { - g.drawString(selectedNode.message, 10, 10); - } + g.setColor(Color.black); + if (n.isSelected) { + BasicGraphicsUtils.drawDashedRect(g, n.x - delta, n.y - delta, + 2 * delta, 2 * delta); + } + if (selectedNode != null && selectedNode.message != null) { + g.drawString(selectedNode.message, 10, 10); } } } @@ -382,71 +486,145 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous public void actionPerformed(ActionEvent e) { Object source = e.getSource(); - if (source == timer) { - ticker++; - if (hasPendingEvents) { - boolean repaint = false; - hasPendingEvents = false; - MapNode[] nodes = nodeList; - if (nodes != null) { - long time = System.currentTimeMillis(); - for (int i = 0, n = nodes.length; i < n; i++) { - if (nodes[i].tick(time)) { - repaint = true; - } - } - } - if (repaint) { - hasPendingEvents = true; - repaint(); - } - } else if ((ticker % 10) == 0) { + if (!isMap && source == timer) { + if (isLayoutActive) { + updateNodeLayout(); repaint(); } - } else if (source == hideItem) { - hideNetwork = !hideNetwork; - if (!hideNetwork) hideItem.setText("Hide Network"); - else hideItem.setText("Show Network"); + + } else if (!isMap && source == lockedItem) { + if (popupNode != null) { + popupNode.hasFixedLocation = lockedItem.isSelected(); + } + + } else if (!isMap && source == layoutItem) { + isLayoutActive = layoutItem.isSelected(); + + } else if (!isMap && source == shakeItem) { + for(MapNode n : getNodeList()) { + if (!n.hasFixedLocation) { + n.x += Math.random() * 100 - 50; + n.y += Math.random() * 100 - 50; + } + } + + } else if (!isMap && source == configItem) { + if (configItem.isSelected()) { + configPanel.setSize(getPreferredSize()); + configPanel.validate(); + updateConfigLayout(); + configPanel.setVisible(true); + } else { + configPanel.setVisible(false); + } + repaint(); + + } else if (source == showNetworkItem) { + hideNetwork = !showNetworkItem.isSelected(); repaint(); } else if (source == resetNetworkItem) { - MapNode[] nodes = nodeList; - if (nodes != null) { - for (int i = 0, m = nodes.length; i < m; i++) { - MapNode n = nodes[i]; - n.node.clearLinks(); - } - repaint(); + for(MapNode n : getNodeList()) { + n.node.clearLinks(); } + repaint(); + } else if (isMap && source == backgroundItem) { + showBackground = mapImage != null && backgroundItem.isSelected(); + repaint(); } } + private void updateNodeLayout() { + MapNode[] nodes = getNodeList(); + for (MapNode n : nodes) { + + // Attract connected nodes + for(int i = 0, jn = n.node.getLinkCount(); i < jn; i++) { + Link link = n.node.getLink(i); + MapNode n2 = addMapNode(link.node); + double vx = n2.x - n.x; + double vy = n2.y - n.y; + double dist = Math.sqrt(vx * vx + vy * vy); + dist = dist == 0 ? 0.00001 : dist; + double etx = link.getETX(); + if (etx > 5) etx = 5; + double factor = (etx * layoutAttract - dist) / (dist * 3); + double dx = factor * vx; + double dy = factor * vy; + + n2.dx += dx; + n2.dy += dy; + n.dx -= dx; + n.dy -= dy; + } + + // Repel nodes that are too close + double dx = 0, dy = 0; + for (MapNode n2 : nodes) { + if (n == n2) { + continue; + } + double vx = n.x - n2.x; + double vy = n.y - n2.y; + double dist = vx * vx + vy * vy; + if (dist == 0) { + dx += Math.random() * 5; + dy += Math.random() * 5; + } else if (dist < layoutRepel * layoutRepel) { + dx += vx / dist; + dy += vy / dist; + } + } + double dist = dx * dx + dy * dy; + if (dist > 0) { + dist = Math.sqrt(dist) / 2; + n.dx += dx / dist; + n.dy += dy / dist; + } + + n.dy += layoutGravity; + } + + // Update the node positions + int width = getWidth(); + int height = getHeight(); + for(MapNode n : nodes) { + if (!n.hasFixedLocation && n != draggedNode) { + n.x += Math.max(-5, Math.min(5, n.dx)); + n.y += Math.max(-5, Math.min(5, n.dy)); + if (n.x < 0) { + n.x = 0; + } else if (n.x > width) { + n.x = width; + } + if (n.y < 0) { + n.y = 0; + } else if (n.y > height) { + n.y = height; + } + } + n.dx /= 2; + n.dy /= 2; + } + } + + private void updateConfigLayout() { + configPanel.setLocation(getWidth() - configPanel.getWidth() - 10, + getHeight() - configPanel.getHeight() - 10); + } + // ------------------------------------------------------------------- // Mouselistener // ------------------------------------------------------------------- private MapNode getNodeAt(int mx, int my) { - int lx = 10; - MapNode[] nodes = nodeList; - if (nodes != null) { - for (int i = 0, m = nodes.length; i < m; i++) { - MapNode n = nodes[i]; - int x, y; - if (n.node.hasLocation()) { - x = n.node.getX(); - y = n.node.getY(); - } else { - x = lx; - y = 10; - lx += 30; - } - if (mx >= (x - delta) - && mx <= (x + delta) - && my >= (y - delta) - && my <= (y + delta)) { - return n; - } + for(MapNode n : getNodeList()) { + if (mx >= (n.x - delta) + && mx <= (n.x + delta) + && my >= (n.y - delta) + && my <= (n.y + delta)) { + return n; } } return null; @@ -491,8 +669,8 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous // Do not drag if mouse is only moved during click } else { - draggedNode.node.setLocation(e.getX(), e.getY()); - server.updateNodeLocation(draggedNode.node); + draggedNode.x = e.getX(); + draggedNode.y = e.getY(); setCursor(Cursor.getDefaultCursor()); draggedTime = 0; draggedNode = null; @@ -506,8 +684,11 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous private void showPopup(MouseEvent e) { if (e.isPopupTrigger() && (e.getModifiers() & (MouseEvent.SHIFT_MASK|MouseEvent.CTRL_MASK)) == 0) { -// popupNode = getNodeAt(e.getX(), e.getY()); -// nodeItem.setEnabled(popupNode != null); + popupNode = getNodeAt(e.getX(), e.getY()); + if (!isMap) { + lockedItem.setEnabled(popupNode != null); + lockedItem.setSelected(popupNode != null ? popupNode.hasFixedLocation : false); + } popupMenu.show(this, e.getX(), e.getY()); } } @@ -534,8 +715,9 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } - } else if (VISUAL_DRAG) { - draggedNode.node.setLocation(e.getX(), e.getY()); + } else if (VISUAL_DRAG /* && dragItem.isSelected() */) { + draggedNode.x = e.getX(); + draggedNode.y = e.getY(); repaint(); } } @@ -551,60 +733,44 @@ public class MapPanel extends JPanel implements Visualizer, ActionListener, Mous private static class MapNode { public final Node node; + public int x; + public int y; + public double dx; + public double dy; + public boolean hasFixedLocation; public boolean isSelected; - public String message = null; - - private int tick = 0; + public String message; MapNode(MapPanel panel, Node node) { this.node = node; } - boolean tick(long time) { - boolean r = false; - if (tick > 0) { - tick--; - r = true; - } - - for (int i = 0, n = node.getLinkCount(); i < n; i++) { - Link link = node.getLink(i); - long age = (time - link.getLastActive()) / 100; - if (age < 200) { - r = true; - break; - } - } - return r; - } - public void paint(Graphics g, int x, int y) { - if (tick > (TOTAL_SHOW - SHOW_BLINK)) { - if ((tick & 4) == 0) { - // Hide circle - } else { - int index = FADE_COUNT - tick - 1; - if (index < 0) { - index = 0; - } - final int d = 8; - g.setColor(OTHER_COLOR[index]); - g.fillOval(x - d, y - d, d * 2 + 1, d * 2 + 1); - } - } - - if (tick < (TOTAL_SHOW - SHOW_BLINK) && tick > 0) { - g.setColor(Color.red); - int height = 13 * tick / TOTAL_SHOW; - g.fillRect(x - 6, 5 + y - height, 2, height); - } - - g.setColor(Color.black); final int od = 3; + g.setColor(Color.black); g.drawString(node.getID(), x + od * 2 + 3, y + 4); + if (hasFixedLocation) { + g.setColor(Color.red); + } g.fillOval(x - od, y - od, od * 2 + 1, od * 2 + 1); } } // end of inner class MapNode + + @Override + public void updateConfig(Properties config) { + if (isMap) { + for (MapNode n : getNodeList()) { + config.put(n.node.getID(), "" + n.x + ',' + n.y); + } + } else { + for (MapNode n : getNodeList()) { + if (n.hasFixedLocation) { + config.put("collect.map." + n.node.getID(), "" + n.x + ',' + n.y); + } + } + } + } + }