diff --git a/src/java/com/threerings/nodemap/DuplicateEdgeException.java b/src/java/com/threerings/nodemap/DuplicateEdgeException.java new file mode 100644 index 000000000..86b3f9262 --- /dev/null +++ b/src/java/com/threerings/nodemap/DuplicateEdgeException.java @@ -0,0 +1,16 @@ +// +// $Id: DuplicateEdgeException.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +/** + * Thrown when an action is attempted on an edge that is already + * present in the involved node. + */ +public class DuplicateEdgeException extends NodeMapException +{ + public DuplicateEdgeException () + { + super("error.duplicate_edge"); + } +} diff --git a/src/java/com/threerings/nodemap/DuplicateNodeException.java b/src/java/com/threerings/nodemap/DuplicateNodeException.java new file mode 100644 index 000000000..d05d1962c --- /dev/null +++ b/src/java/com/threerings/nodemap/DuplicateNodeException.java @@ -0,0 +1,16 @@ +// +// $Id: DuplicateNodeException.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +/** + * Thrown when an action is attempted on a node that is already + * present in the node map. + */ +public class DuplicateNodeException extends NodeMapException +{ + public DuplicateNodeException () + { + super("error.duplicate_node"); + } +} diff --git a/src/java/com/threerings/nodemap/Edge.java b/src/java/com/threerings/nodemap/Edge.java new file mode 100644 index 000000000..619589c65 --- /dev/null +++ b/src/java/com/threerings/nodemap/Edge.java @@ -0,0 +1,59 @@ +// +// $Id: Edge.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +import java.awt.Graphics; + +/** + * The edge class represents an edge connecting two nodes. + */ +public abstract class Edge +{ + /** The source node. */ + public Node src; + + /** The destination node. */ + public Node dst; + + /** + * Construct an edge object connecting the given nodes. + * + * @param src the source node. + * @param dst the destination node. + */ + public Edge (Node src, Node dst) + { + this.src = src; + this.dst = dst; + } + + /** + * Paint the edge to the given graphics context. + * + * @param g the graphics context. + */ + public abstract void paint (Graphics g); + + /** + * Return a string representation of this edge. + */ + public String toString () + { + StringBuffer buf = new StringBuffer(); + buf.append("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * This should be overridden by derived classes (which should be sure + * to call super.toString()) to append the derived class + * specific event information to the string buffer. + */ + public void toString (StringBuffer buf) + { + buf.append("src=").append(src); + buf.append(", dst=").append(dst); + } +} diff --git a/src/java/com/threerings/nodemap/LayoutManager.java b/src/java/com/threerings/nodemap/LayoutManager.java new file mode 100644 index 000000000..d646155c1 --- /dev/null +++ b/src/java/com/threerings/nodemap/LayoutManager.java @@ -0,0 +1,24 @@ +// +// $Id: LayoutManager.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +import java.util.List; + +/** + * The layout manager class provides an interface that lays out nodes + * in a node map via some preordained desirable methodology. + */ +public interface LayoutManager +{ + /** + * Lay out the nodes in the list in the fashion dictated by this + * particular layout manager. The node positions are updated via + * {@link Node#setPosition} such that subsequent rendering of the + * nodes will place them in appropriate positions. + * + * @param root the root node for the graph. + * @param nodes the list of nodes. + */ + public void layout (Node root, List nodes); +} diff --git a/src/java/com/threerings/nodemap/Log.java b/src/java/com/threerings/nodemap/Log.java new file mode 100644 index 000000000..6dce46501 --- /dev/null +++ b/src/java/com/threerings/nodemap/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +/** + * A placeholder class that contains a reference to the log object used by + * the nodemap package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("nodemap"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/nodemap/NoSuchNodeException.java b/src/java/com/threerings/nodemap/NoSuchNodeException.java new file mode 100644 index 000000000..5ef4c9c20 --- /dev/null +++ b/src/java/com/threerings/nodemap/NoSuchNodeException.java @@ -0,0 +1,15 @@ +// +// $Id: NoSuchNodeException.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +/** + * Thrown when a node cannot be found in the node map. + */ +public class NoSuchNodeException extends NodeMapException +{ + public NoSuchNodeException () + { + super("error.no_such_node"); + } +} diff --git a/src/java/com/threerings/nodemap/Node.java b/src/java/com/threerings/nodemap/Node.java new file mode 100644 index 000000000..d983015c1 --- /dev/null +++ b/src/java/com/threerings/nodemap/Node.java @@ -0,0 +1,179 @@ +// +// $Id: Node.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +import java.awt.Graphics; +import java.awt.Point; +import java.util.*; + +/** + * The node class represents a single node in a directed graph of nodes. + * + * @see NodeMap + */ +public abstract class Node +{ + /** + * Construct a node with the given attributes. + * + * @param name the node name. + * @param width the width in pixels. + * @param height the height in pixels. + */ + public Node (String name, int width, int height) + { + _name = name; + _width = width; + _height = height; + + _pos = new Point(); + _edges = new ArrayList(); + } + + /** + * Add a connecting edge to this node. An edge may not be + * added to the node more than once. + * + * @param e the new edge. + */ + protected void addEdge (Edge e) throws DuplicateEdgeException + { + // don't allow adding the same edge object twice + if (_edges.contains(e)) { + throw new DuplicateEdgeException(); + } + + // save off the new connecting edge + _edges.add(e); + } + + /** + * Return an Iterator object that iterates over the + * edges leaving this node. + * + * @return the iterator object. + */ + public Iterator getEdges () + { + return (Collections.unmodifiableList(_edges)).iterator(); + } + + /** + * Return the node's current position in pixels. + * + * @return point the node position. + */ + public Point getPosition () + { + return _pos; + } + + /** + * Set the node's position in pixels. + * + * @param x the x-position. + * @param y the y-position. + */ + public void setPosition (int x, int y) + { + _pos.setLocation(x, y); + } + + /** + * Return whether the node contains the given point. + * + * @param x the x-position. + * @param y the y-position. + * + * @return true if the node contains the point, false if not. + */ + public boolean contains (int x, int y) + { + return (x >= _pos.x && x <= (_pos.x + _width) && + y >= _pos.y && y <= (_pos.y + _height)); + } + + /** + * Return the node width in pixels. + */ + public int getWidth () + { + return _width; + } + + /** + * Return the node height in pixels. + */ + public int getHeight () + { + return _height; + } + + /** + * Render the node to the given graphics context. + * + * @param g the graphics context. + */ + public abstract void paint (Graphics g); + + /** + * Render the node's connecting edges to the given graphics context. + * + * @param g the graphics context. + */ + public void paintEdges (Graphics g) + { + int size = _edges.size(); + for (int ii = 0; ii < size; ii++) { + ((Edge)_edges.get(ii)).paint(g); + } + } + + /** + * Handle mouse-entered events for this node. + */ + public void handleMouseEntered () + { + Log.info("handleMouseEntered [n=" + this + "]."); + } + + /** + * Handle mouse-exited events for this node. + */ + public void handleMouseExited () + { + Log.info("handleMouseExited [n=" + this + "]."); + } + + /** + * Handle mouse-clicked events for this node. + * + * @param x the x-coordinate in intra-node pixels. + * @param y the y-coordinate in intra-node pixels. + */ + public void handleMouseClicked (int x, int y) + { + Log.info("handleMouseClicked [n=" + this + "]."); + } + + /** + * Return a string representation of this node. + */ + public String toString () + { + return _name; + } + + /** The position in pixel coordinates. */ + protected Point _pos; + + /** The dimensions in pixel coordinates. */ + protected int _width, _height; + + /** The node name. */ + protected String _name; + + /** The edges leaving this node. */ + protected ArrayList _edges; +} diff --git a/src/java/com/threerings/nodemap/NodeMap.java b/src/java/com/threerings/nodemap/NodeMap.java new file mode 100644 index 000000000..b4a0634a7 --- /dev/null +++ b/src/java/com/threerings/nodemap/NodeMap.java @@ -0,0 +1,270 @@ +// +// $Id: NodeMap.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +import java.awt.*; +import java.util.*; + +/** + * The node map class represents a directed graph of nodes comprised + * of {@link Node} objects. A user-specifiable {@link LayoutManager} + * is used to lay out the nodes in the graph. + */ +public class NodeMap +{ + /** + * Construct a node map object with the given layout manager. + * + * @param lgmr the layout manager. + */ + public NodeMap (LayoutManager lmgr) + { + _lmgr = lmgr; + _nodes = new ArrayList(); + _dirty = false; + } + + /** + * Force the nodes in the map to be re-laid out. + */ + public void layout () + { + // do nothing if we have no nodes + if (_nodes.size() == 0) return; + + // default to using the first node as the root + if (_root == null) { + _root = (Node)_nodes.get(0); + } + + // lay out the nodes + _lmgr.layout(_root, _nodes); + + // update bounding node positions + updateBounds(); + + // note that we're no longer dirty + _dirty = false; + } + + /** + * Calculate the bounding rectangle that wholly contains the node map. + */ + protected void updateBounds () + { + Iterator iter = _nodes.iterator(); + while (iter.hasNext()) { + Node n = (Node)iter.next(); + Point pos = n.getPosition(); + int wid = n.getWidth(), hei = n.getHeight(); + + _minx = Math.min(_minx, pos.x); + _maxx = Math.max(_maxx, pos.x + wid); + _miny = Math.min(_miny, pos.y); + _maxy = Math.max(_maxy, pos.y + hei); + } + } + + /** + * Render the node map to the given graphics context. + * + * @param g the graphics context. + */ + public void paint (Graphics g) + { + // lay out the nodes if necessary + if (_dirty) { + layout(); + } + + // translate context to node map origin + g.translate(-_minx, -_miny); + + // render all node edges + int size = _nodes.size(); + for (int ii = 0; ii < size; ii++) { + Node n = (Node)_nodes.get(ii); + n.paintEdges(g); + } + + // render all nodes + for (int ii = 0; ii < size; ii++) { + Node n = (Node)_nodes.get(ii); + n.paint(g); + } + + // highlight the last node entered + if (_lastnode != null) { + g.setColor(Color.red); + Point pos = _lastnode.getPosition(); + int wid = _lastnode.getWidth(), hei = _lastnode.getHeight(); + g.drawRect(pos.x, pos.y, wid, hei); + } + + // restore original origin + g.translate(_minx, _miny); + } + + /** + * Add the given node to the node map. A given node may not be + * present in the node map more than once. + * + * @param n the node to add. + */ + public void addNode (Node n) throws DuplicateNodeException + { + // don't allow adding a node more than once + if (_nodes.contains(n)) { + throw new DuplicateNodeException(); + } + + // add the node + _nodes.add(n); + _dirty = true; + } + + /** + * Add the given edge to the node. Edges may not be added to a + * node more than once, or to a node that is not present in the + * node map. + * + * @param n the node. + * @param e the edge to add. + */ + public void addEdge (Node n, Edge e) + throws NoSuchNodeException, DuplicateEdgeException + { + // don't allow adding an edge to an unknown node + if (!_nodes.contains(n)) { + throw new NoSuchNodeException(); + } + + // add the edge to the node + n.addEdge(e); + _dirty = true; + } + + /** + * Set the root node for the node map. The given node must be + * present in the node map. + * + * @param n the root node. + */ + public void setRootNode (Node n) throws NoSuchNodeException + { + if (!_nodes.contains(n)) { + throw new NoSuchNodeException(); + } + + _root = n; + _dirty = true; + } + + /** + * Return the dimensions of the node map in pixels. + */ + public Dimension getSize () + { + // lay out the nodes if necessary + if (_dirty) { + layout(); + } + + return new Dimension(_maxx - _minx, _maxy - _miny); + } + + /** + * Handle mouse-moved events passed on by the containing panel. + * Inform any affected nodes of mouse-entered and mouse-exited + * events. + * + * @param x the mouse x-coordinate. + * @param y the ymouse y-coordinate. + */ + public void handleMouseMoved (int x, int y) + { + Node enternode = null; + + // translate coordinates to node map origin + x += _minx; + y += _miny; + + int size = _nodes.size(); + for (int ii = 0; ii < size; ii++) { + Node n = (Node)_nodes.get(ii); + + if (n.contains(x, y)) { + + // bail if we're in the same node we were in last + if (_lastnode == n) return; + + // note that we're entering this node + enternode = n; + + break; + } + } + + // exit the last node, if any + if (_lastnode != null) { + _lastnode.handleMouseExited(); + } + + // enter the new node, if any + if (enternode != null) { + enternode.handleMouseEntered(); + } + + // update the last node entered + _lastnode = enternode; + } + + /** + * Handle mouse-clicked events passed on by the containing panel. + * Inform any affected nodes of mouse-clicked events. + * + * @param x the mouse x-coordinate. + * @param y the ymouse y-coordinate. + */ + public void handleMouseClicked (int x, int y) + { + // translate coordinates to node map origin + x += _minx; + y += _miny; + + int size = _nodes.size(); + for (int ii = 0; ii < size; ii++) { + Node n = (Node)_nodes.get(ii); + + if (n.contains(x, y)) { + + // inform the node of the mouse click, with + // coordinates translated to suit node interior + Point pos = n.getPosition(); + n.handleMouseClicked(x - pos.x, y - pos.y); + + // nodes can't overlap for now, so we're done + return; + } + } + } + + /** The layout manager. */ + protected LayoutManager _lmgr; + + /** The nodes in the node map. */ + protected ArrayList _nodes; + + /** The root node. */ + protected Node _root; + + /** The bounding box coordinates. */ + protected int _minx, _maxx, _miny, _maxy; + + /** The last node the mouse entered. */ + protected Node _lastnode; + + /** Whether the node map needs to be laid out. */ + protected boolean _dirty; +} diff --git a/src/java/com/threerings/nodemap/NodeMapException.java b/src/java/com/threerings/nodemap/NodeMapException.java new file mode 100644 index 000000000..d6cf688ec --- /dev/null +++ b/src/java/com/threerings/nodemap/NodeMapException.java @@ -0,0 +1,16 @@ +// +// $Id: NodeMapException.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +/** + * Thrown when an error occurs while performing certain activities + * involving a node map object. + */ +public class NodeMapException extends Exception +{ + public NodeMapException (String message) + { + super(message); + } +} diff --git a/src/java/com/threerings/nodemap/NodeMapPanel.java b/src/java/com/threerings/nodemap/NodeMapPanel.java new file mode 100644 index 000000000..d13af7487 --- /dev/null +++ b/src/java/com/threerings/nodemap/NodeMapPanel.java @@ -0,0 +1,61 @@ +// +// $Id: NodeMapPanel.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; + +/** + * The node map panel handles display of a node map and passes Swing + * UI events on to the appropriate node map event handling methods. + */ +public class NodeMapPanel extends JPanel + implements MouseListener, MouseMotionListener +{ + public NodeMapPanel (NodeMap map) + { + _map = map; + + // listen to our mouse events + addMouseListener(this); + addMouseMotionListener(this); + } + + public void paintComponent (Graphics g) + { + super.paintComponent(g); + _map.paint(g); + } + + public Dimension getPreferredSize () + { + return _map.getSize(); + } + + /** MouseListener interface methods */ + + public void mouseClicked (MouseEvent e) + { + _map.handleMouseClicked(e.getX(), e.getY()); + } + + public void mousePressed (MouseEvent e) { } + public void mouseReleased (MouseEvent e) { } + public void mouseEntered (MouseEvent e) { } + public void mouseExited (MouseEvent e) { } + + /** MouseMotionListener interface methods */ + + public void mouseMoved (MouseEvent e) + { + _map.handleMouseMoved(e.getX(), e.getY()); + repaint(); + } + + public void mouseDragged (MouseEvent e) { } + + /** The node map. */ + protected NodeMap _map; +} diff --git a/src/java/com/threerings/nodemap/direction/DirectionalEdge.java b/src/java/com/threerings/nodemap/direction/DirectionalEdge.java new file mode 100644 index 000000000..c7d0a579c --- /dev/null +++ b/src/java/com/threerings/nodemap/direction/DirectionalEdge.java @@ -0,0 +1,35 @@ +// +// $Id: DirectionalEdge.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap.direction; + +import java.awt.*; + +import com.threerings.nodemap.*; + +public class DirectionalEdge extends Edge +{ + /** The edge direction as a {@link Directions} constant. */ + public int dir; + + public DirectionalEdge (Node src, Node dst, int dir) + { + super(src, dst); + this.dir = dir; + } + + public void paint (Graphics g) + { + g.setColor(Color.black); + + Point spos = src.getPosition(), dpos = dst.getPosition(); + + int csx = spos.x + (src.getWidth() / 2); + int csy = spos.y + (src.getHeight() / 2); + + int cdx = dpos.x + (dst.getWidth() / 2); + int cdy = dpos.y + (dst.getHeight() / 2); + + g.drawLine(csx, csy, cdx, cdy); + } +} diff --git a/src/java/com/threerings/nodemap/direction/DirectionalLayoutManager.java b/src/java/com/threerings/nodemap/direction/DirectionalLayoutManager.java new file mode 100644 index 000000000..2f164a7ab --- /dev/null +++ b/src/java/com/threerings/nodemap/direction/DirectionalLayoutManager.java @@ -0,0 +1,100 @@ +// +// $Id: DirectionalLayoutManager.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap.direction; + +import java.util.*; + +import com.threerings.nodemap.*; + +/** + * The directional layout manager lays nodes out according to the + * eight cardinal directions as specified in the {@link Directions} + * class. The nodes must be connected with {@link DirectionalEdge} + * edges. + */ +public class DirectionalLayoutManager implements LayoutManager +{ + public DirectionalLayoutManager (int edgelen) + { + _edgelen = edgelen; + _closed = new HashMap(); + } + + public void layout (Node root, List nodes) + { + _closed.clear(); + + // lay out all nodes + layoutNode(root, 0, 0, root); + } + + protected void layoutNode (Node root, int x, int y, Node n) + { + // consider each node only once for now + if (_closed.containsKey(n)) { + return; + } + + // set the node's location + n.setPosition(x, y); + + // remember that we've placed this node + _closed.put(n, null); + + Iterator iter = n.getEdges(); + while (iter.hasNext()) { + DirectionalEdge e = (DirectionalEdge)iter.next(); + + int tx = x, ty = y; + int wid = e.dst.getWidth(), hei = e.dst.getHeight(); + + // shift the node in the appropriate direction + switch (e.dir) { + case Directions.NORTH: + tx -= (_edgelen + wid); + ty -= (_edgelen + hei); + break; + + case Directions.NORTHEAST: + ty -= (_edgelen + hei); + break; + + case Directions.EAST: + tx += (_edgelen + wid); + ty -= (_edgelen + hei); + break; + + case Directions.SOUTHEAST: + tx += (_edgelen + wid); + break; + + case Directions.SOUTH: + tx += (_edgelen + wid); + ty += (_edgelen + hei); + break; + + case Directions.SOUTHWEST: + ty += (_edgelen + hei); + break; + + case Directions.WEST: + tx -= (_edgelen + wid); + ty += (_edgelen + hei); + break; + + case Directions.NORTHWEST: + tx -= (_edgelen + wid); + break; + } + + layoutNode(root, tx, ty, e.dst); + } + } + + /** The length of the edges between nodes in pixels. */ + protected int _edgelen; + + /** The set of nodes already positioned. */ + protected HashMap _closed; +} diff --git a/src/java/com/threerings/nodemap/direction/Directions.java b/src/java/com/threerings/nodemap/direction/Directions.java new file mode 100644 index 000000000..66f8ed329 --- /dev/null +++ b/src/java/com/threerings/nodemap/direction/Directions.java @@ -0,0 +1,24 @@ +// +// $Id: Directions.java,v 1.1 2001/08/20 22:56:55 shaper Exp $ + +package com.threerings.nodemap.direction; + +/** + * This class provides a holding place for constants referenced by the + * classes that support and make use of directional node maps. + */ +public class Directions +{ + /** Constants for the eight cardinal compass-point directions. */ + public static final int NORTH = 0; + public static final int NORTHEAST = 1; + public static final int EAST = 2; + public static final int SOUTHEAST = 3; + public static final int SOUTH = 4; + public static final int SOUTHWEST = 5; + public static final int WEST = 6; + public static final int NORTHWEST = 7; + + /** The number of directions. */ + public static final int NUM_DIRECTIONS = 8; +}