0001: /*
0002: * WebSphinx web-crawling toolkit
0003: *
0004: * Copyright (c) 1998-2002 Carnegie Mellon University. All rights
0005: * reserved.
0006: *
0007: * Redistribution and use in source and binary forms, with or without
0008: * modification, are permitted provided that the following conditions
0009: * are met:
0010: *
0011: * 1. Redistributions of source code must retain the above copyright
0012: * notice, this list of conditions and the following disclaimer.
0013: *
0014: * 2. Redistributions in binary form must reproduce the above copyright
0015: * notice, this list of conditions and the following disclaimer in
0016: * the documentation and/or other materials provided with the
0017: * distribution.
0018: *
0019: * THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND
0020: * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
0021: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
0022: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY
0023: * NOR ITS EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
0024: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
0025: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
0026: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
0027: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0028: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
0029: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0030: *
0031: */
0032:
0033: package websphinx.workbench;
0034:
0035: // Daniel Tunkelang's graph-drawing packages
0036: import graph.*;
0037: import gd.*;
0038:
0039: import rcm.awt.*;
0040:
0041: import java.util.*;
0042: import java.awt.*;
0043: import java.awt.image.ImageObserver;
0044:
0045: public class GraphLayout extends Canvas implements Runnable,
0046: ImageObserver {
0047:
0048: Graph graph; // graph to display
0049:
0050: double restLength = 50; // default rest length of an edge
0051: double springConstant = 100; // attraction between connected nodes
0052: double nodeCharge = 10000; // repulsion between any pair of nodes
0053:
0054: GDAlgorithm algorithm; // algorithm used for automatic graph layout
0055:
0056: boolean running = false; // is repaint thread running?
0057: boolean automaticLayout = true; // is automatic graph layout enabled?
0058: double threshold = 100; // if an iteration shows less "improvement" than
0059: // this threshold, stop iterating
0060: boolean quiescent = true; // is the graph stable?
0061: boolean dirty = true; // do we need to repaint?
0062:
0063: int interval = 100; // milliseconds between repaints
0064: int iterations = 3; // number of layout iterations per repaint
0065:
0066: Color nodeColor = Color.pink; // default background color for a node
0067: // (node label text is in Foreground color)
0068: Color edgeColor = Color.black; // default color of an edge line
0069: Color tipColor = Color.yellow; // background color of a popup tip
0070:
0071: //RenderedNode selectedNode = null; // currently selected node, or null
0072: //RenderedEdge selectedEdge = null; // currently selected edge, or null
0073: // invariant: selectedNode==null || selectedEdge == null
0074:
0075: Object tipObject = null; // node or edge currently under mouse, or null
0076: MultiLineString tip = null; // tip string displayed for tipObject, or null
0077: int tipX, tipY, tipWidth, tipHeight; // bounding box of tip
0078:
0079: GraphLayoutControlPanel controlPanel;
0080:
0081: /**
0082: * Make a GraphLayout.
0083: */
0084: public GraphLayout() {
0085: graph = new Graph();
0086: resetAlgorithm();
0087: start();
0088: }
0089:
0090: /**
0091: * Erase the graph.
0092: */
0093: public synchronized void clear() {
0094: graph = new Graph();
0095: changedGraph();
0096: }
0097:
0098: /**
0099: * Get the graph.
0100: */
0101: public synchronized Graph getGraph() {
0102: return graph;
0103: }
0104:
0105: /**
0106: * Set the graph.
0107: */
0108: public synchronized void setGraph(Graph graph) {
0109: this .graph = graph;
0110: //selectedNode = null;
0111: //selectedEdge = null;
0112: tipObject = null;
0113: tip = null;
0114: changedGraph();
0115: }
0116:
0117: /**
0118: * Get the graph-drawing algorithm in use.
0119: */
0120: public synchronized GDAlgorithm getAlgorithm() {
0121: return algorithm;
0122: }
0123:
0124: /**
0125: * Set the graph-drawing algorithm.
0126: */
0127: public synchronized void setAlgorithm(GDAlgorithm algorithm) {
0128: this .algorithm = algorithm;
0129: changedGraph();
0130: }
0131:
0132: synchronized void resetAlgorithm() {
0133: algorithm = new AllPairsAlgorithm(springConstant, nodeCharge);
0134: changedGraph();
0135: }
0136:
0137: /**
0138: * Get the default rest length for new edges.
0139: */
0140: public synchronized double getRestLength() {
0141: return restLength;
0142: }
0143:
0144: /**
0145: * Set the default rest length for new edges.
0146: */
0147: public synchronized void setRestLength(double restLength) {
0148: this .restLength = restLength;
0149: changedGraph();
0150: }
0151:
0152: /**
0153: * Get the spring constant.
0154: */
0155: public synchronized double getSpringConstant() {
0156: return springConstant;
0157: }
0158:
0159: /**
0160: * Set the spring constant.
0161: */
0162: public synchronized void setSpringConstant(double springConstant) {
0163: this .springConstant = springConstant;
0164: resetAlgorithm();
0165: }
0166:
0167: /**
0168: * Get the node charge.
0169: */
0170: public synchronized double getNodeCharge() {
0171: return nodeCharge;
0172: }
0173:
0174: /**
0175: * Set the node charge.
0176: */
0177: public synchronized void setNodeCharge(double nodeCharge) {
0178: this .nodeCharge = nodeCharge;
0179: resetAlgorithm();
0180: }
0181:
0182: /**
0183: * Get the refresh interval (measured in seconds).
0184: */
0185: public synchronized int getInterval() {
0186: return interval;
0187: }
0188:
0189: /**
0190: * Set the refresh interval (in seconds).
0191: */
0192: public synchronized void setInterval(int interval) {
0193: this .interval = interval;
0194: }
0195:
0196: /**
0197: * Get the layout algorithm iterations per refresh.
0198: */
0199: public synchronized int getIterations() {
0200: return iterations;
0201: }
0202:
0203: /**
0204: * Set the layout algorithm iterations per refresh.
0205: */
0206: public synchronized void setIterations(int iterations) {
0207: this .iterations = iterations;
0208: }
0209:
0210: /**
0211: * Test whether the graph is laid out automatically.
0212: */
0213: public synchronized boolean getAutomaticLayout() {
0214: return automaticLayout;
0215: }
0216:
0217: /**
0218: * Set whether the graph is laid out automatically.
0219: */
0220: public synchronized void setAutomaticLayout(boolean f) {
0221: automaticLayout = f;
0222: quiescent = !automaticLayout;
0223: if (controlPanel != null)
0224: controlPanel.automatic.setState(automaticLayout);
0225: }
0226:
0227: /**
0228: * Test whether the graph is quiescent (not changing in the background).
0229: */
0230: public synchronized boolean getQuiescent() {
0231: return quiescent;
0232: }
0233:
0234: /**
0235: * Test whether the graph layout thread is running in the background
0236: */
0237: public synchronized boolean getRunning() {
0238: return running;
0239: }
0240:
0241: /**
0242: * Get the threshold.
0243: */
0244: public synchronized double getThreshold() {
0245: return threshold;
0246: }
0247:
0248: /**
0249: * Set the threshold.
0250: */
0251: public synchronized void setThreshold(double threshold) {
0252: this .threshold = threshold;
0253: changedGraph();
0254: }
0255:
0256: /**
0257: * Get the node background color.
0258: */
0259: public synchronized Color getNodeColor() {
0260: return nodeColor;
0261: }
0262:
0263: /**
0264: * Set the node background color.
0265: */
0266: public synchronized void setNodeColor(Color nodeColor) {
0267: this .nodeColor = nodeColor;
0268: }
0269:
0270: /**
0271: * Get the edge color.
0272: */
0273: public synchronized Color getEdgeColor() {
0274: return edgeColor;
0275: }
0276:
0277: /**
0278: * Set the edge color.
0279: */
0280: public synchronized void setEdgeColor(Color edgeColor) {
0281: this .edgeColor = edgeColor;
0282: }
0283:
0284: /**
0285: * Get the popup tip color.
0286: */
0287: public synchronized Color getTipColor() {
0288: return tipColor;
0289: }
0290:
0291: /**
0292: * Set the popup tip color.
0293: */
0294: public synchronized void setTipColor(Color tipColor) {
0295: this .tipColor = tipColor;
0296: }
0297:
0298: /**
0299: * Get node currently under the mouse pointer, or null if no node is under the mouse.
0300: */
0301: public synchronized RenderedNode getSelectedNode() {
0302: return tipObject instanceof RenderedNode ? (RenderedNode) tipObject
0303: : null;
0304: }
0305:
0306: /**
0307: * Get edge currently under the mouse pointer, or null if no edge is under the mouse.
0308: */
0309: public synchronized RenderedEdge getSelectedEdge() {
0310: return tipObject instanceof RenderedEdge ? (RenderedEdge) tipObject
0311: : null;
0312: }
0313:
0314: /**
0315: * Add a node.
0316: */
0317: public synchronized void addNode(RenderedNode node) {
0318: graph.addNode(node);
0319: graph.placeNode(node, node.x, node.y);
0320: changedGraph();
0321: }
0322:
0323: /**
0324: * Add an edge.
0325: */
0326: public synchronized void addEdge(RenderedEdge edge) {
0327: if (edge.restLength == 0)
0328: edge.restLength = restLength;
0329: graph.addEdge(edge);
0330: changedGraph();
0331: }
0332:
0333: /**
0334: * Remove a node.
0335: */
0336: public synchronized void removeNode(RenderedNode node) {
0337: graph.removeNode(node);
0338: changedGraph();
0339: }
0340:
0341: /**
0342: * Remove an edge.
0343: */
0344: public synchronized void removeEdge(RenderedEdge edge) {
0345: graph.removeEdge(edge);
0346: changedGraph();
0347: }
0348:
0349: /**
0350: * Handle a loaded image.
0351: */
0352: public synchronized boolean imageUpdate(Image img, int infoflags,
0353: int x, int y, int width, int height) {
0354: if ((infoflags & (ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0) {
0355: for (int i = 0; i < graph.sizeNodes; ++i) {
0356: RenderedNode n = (RenderedNode) graph.nodes[i];
0357: if (n.icon == img) {
0358: n.width = width;
0359: n.height = height;
0360: changedGraph();
0361: }
0362: }
0363: }
0364: return super .imageUpdate(img, infoflags, x, y, width, height);
0365: }
0366:
0367: /*
0368: * Background thread
0369: *
0370: */
0371:
0372: Thread iterator;
0373:
0374: /**
0375: * Start automatic graph layout (in the background).
0376: */
0377: public synchronized void start() {
0378: if (!running) {
0379: running = true;
0380: iterator = new Thread(this , "GraphListener");
0381: iterator.setDaemon(true);
0382: iterator.setPriority(Thread.MIN_PRIORITY);
0383: iterator.start();
0384: }
0385: }
0386:
0387: /**
0388: * Stop automatic graph layout.
0389: */
0390: public synchronized void stop() {
0391: if (running) {
0392: running = false;
0393: notify();
0394: iterator = null;
0395: }
0396: }
0397:
0398: /**
0399: * The body of the background thread. Clients should not call this
0400: * method.
0401: */
0402: final static int MULTIPLIER = 2;
0403:
0404: public synchronized void run() {
0405: quiescent = false;
0406: while (running) {
0407: long start = System.currentTimeMillis();
0408:
0409: if (automaticLayout && !quiescent) {
0410: for (int i = 0; i < iterations; ++i) {
0411: double improvement = algorithm.improveGraph(graph);
0412: dirty = true;
0413: if (improvement <= threshold * graph.sizeNodes) {
0414: quiescent = true;
0415: break;
0416: }
0417: }
0418: }
0419:
0420: if (dirty)
0421: super .repaint();
0422:
0423: /*int r = (int) (System.currentTimeMillis() - start);
0424: int w = Math.max (interval, r * MULTIPLIER);
0425: System.out.println ("ran " + r + " msec, now waiting " + w + " msec");
0426: */
0427:
0428: try {
0429: wait(interval);
0430: } catch (InterruptedException e) {
0431: }
0432: }
0433: quiescent = true;
0434: }
0435:
0436: /**
0437: * Notify background thread that the graph has changed.
0438: */
0439: public synchronized void changedGraph() {
0440: if (automaticLayout)
0441: quiescent = false;
0442: repaint();
0443: }
0444:
0445: /**
0446: * Notify background thread that the view has changed.
0447: */
0448: public synchronized void repaint() {
0449: if (!running)
0450: super .repaint();
0451: else
0452: dirty = true;
0453: }
0454:
0455: /**
0456: * Show control panel for changing graph layout parameters.
0457: */
0458: public void showControlPanel() {
0459: if (controlPanel == null)
0460: controlPanel = new GraphLayoutControlPanel(this );
0461: controlPanel.show();
0462: }
0463:
0464: protected void finalize() throws Throwable {
0465: super .finalize();
0466: if (controlPanel != null) {
0467: controlPanel.dispose();
0468: controlPanel = null;
0469: }
0470: }
0471:
0472: /*
0473: * Scale coordinates from graph to screen.
0474: */
0475: double originX = 0.0, originY = 0.0;
0476: double scaleX = 1.0, scaleY = 1.0;
0477:
0478: private void scaleGraph() {
0479: Dimension d = size();
0480: double halfScreenWidth = d.width / 2.0;
0481: double halfScreenHeight = d.height / 2.0;
0482:
0483: double sX = 1.0, sY = 1.0;
0484: for (int i = 0; i < graph.sizeNodes; ++i) {
0485: RenderedNode n = (RenderedNode) graph.nodes[i];
0486: sX = Math.min(sX, (halfScreenWidth - n.width / 2.0)
0487: / (Math.abs(n.x) + 1));
0488: sY = Math.min(sY, (halfScreenHeight - n.height / 2.0)
0489: / (Math.abs(n.y) + 1));
0490: }
0491:
0492: double oX = halfScreenWidth;
0493: double oY = halfScreenHeight;
0494:
0495: for (int i = 0; i < graph.sizeNodes; ++i) {
0496: RenderedNode n = (RenderedNode) graph.nodes[i];
0497: n.screenX = (int) (n.x * sX + oX);
0498: n.screenY = (int) (n.y * sY + oY);
0499: }
0500:
0501: // save the translation for use in placeNodeOnScreen
0502: originX = oX;
0503: originY = oY;
0504: scaleX = sX;
0505: scaleY = sY;
0506: }
0507:
0508: public synchronized void placeNodeOnScreen(RenderedNode n, int x,
0509: int y) {
0510: graph.placeNode(n, (x - originX) / scaleX, (y - originY)
0511: / scaleY);
0512: n.screenX = x;
0513: n.screenY = y;
0514: }
0515:
0516: public synchronized void placeNodeOnGraph(RenderedNode n, double x,
0517: double y) {
0518: graph.placeNode(n, x, y);
0519: n.screenX = (int) (x * scaleX + originX);
0520: n.screenY = (int) (y * scaleY + originY);
0521: }
0522:
0523: /*
0524: * Painting methods
0525: *
0526: */
0527:
0528: Image offscreen; // offscreen drawing area
0529: Dimension offSize; // size of offscreen buffer
0530: Graphics offg; // drawonable associated with offscreen buffer
0531: FontMetrics fm; // font metrics for offscreen buffer
0532:
0533: public void update(Graphics g) {
0534: // don't clear window with background color first
0535: //long before = System.currentTimeMillis ();
0536: paint(g);
0537: //long after = System.currentTimeMillis ();
0538: //System.out.println ("repaint: " + (after - before) + " msec");
0539: }
0540:
0541: void createOffscreenArea(Dimension d) {
0542: offSize = new Dimension(d.width > 0 ? d.width : 1,
0543: d.height > 0 ? d.height : 1);
0544: offscreen = createImage(offSize.width, offSize.height);
0545: offg = offscreen.getGraphics();
0546: offg.setFont(getFont());
0547: fm = offg.getFontMetrics();
0548: }
0549:
0550: public synchronized void paint(Graphics g) {
0551: Dimension d = size();
0552:
0553: if (offscreen == null || d.width != offSize.width
0554: || d.height != offSize.height)
0555: createOffscreenArea(d);
0556:
0557: offg.setColor(getBackground());
0558: offg.fillRect(0, 0, d.width, d.height);
0559:
0560: scaleGraph();
0561:
0562: // paint the edges first
0563: for (int i = 0; i < graph.sizeEdges; ++i) {
0564: RenderedEdge e = (RenderedEdge) graph.edges[i];
0565: if (e == null)
0566: continue;
0567: RenderedNode from = (RenderedNode) e.from;
0568: RenderedNode to = (RenderedNode) e.to;
0569: if (from == null || to == null)
0570: continue;
0571:
0572: Color c = e.color;
0573: if (c == null)
0574: c = edgeColor;
0575:
0576: offg.setColor(c);
0577: drawArrowToBox(offg, (int) from.screenX,
0578: (int) from.screenY, (int) to.screenX,
0579: (int) to.screenY, (int) (to.width / 2),
0580: (int) (to.height / 2), 6, 3, e.thick);
0581: }
0582:
0583: // paint the nodes on top
0584: for (int i = 0; i < graph.sizeNodes; ++i) {
0585: RenderedNode n = (RenderedNode) graph.nodes[i];
0586: if (n == null)
0587: continue;
0588:
0589: int width = (int) n.width;
0590: int height = (int) n.height;
0591: int x = (int) n.screenX - width / 2;
0592: int y = (int) n.screenY - height / 2;
0593: Color c = n.color;
0594:
0595: if (n.icon == null) {
0596: if (c == null)
0597: c = nodeColor;
0598:
0599: offg.setColor(c);
0600: offg.fillRect(x, y, width, height);
0601: offg.setColor(getForeground());
0602: offg.drawRect(x, y, width - 1, height - 1);
0603: offg.drawString(n.name, x + 5, y + 2 + fm.getAscent());
0604: } else {
0605: // NIY: scaling
0606: if (c == null)
0607: offg.drawImage(n.icon, x, y, this );
0608: else
0609: offg.drawImage(n.icon, x, y, c, this );
0610: }
0611: }
0612:
0613: // paint the tip on top
0614: if (tip != null) {
0615: offg.setColor(tipColor);
0616: offg.fillRect(tipX, tipY, tipWidth, tipHeight);
0617: offg.setColor(Color.black);
0618: offg.drawRect(tipX, tipY, tipWidth - 1, tipHeight - 1);
0619: tip.draw(offg, tipX + 5, tipY + 2, Label.LEFT);
0620: }
0621:
0622: // draw border
0623: offg.setColor(quiescent ? getForeground() : Color.red);
0624: offg.drawRect(0, 0, d.width - 1, d.height - 1);
0625:
0626: // copy to screen
0627: g.drawImage(offscreen, 0, 0, null);
0628:
0629: dirty = false;
0630: }
0631:
0632: void drawArrowToBox(Graphics g, int x1, int y1, int x2, int y2,
0633: int wHalfBox, int hHalfBox, int head_length,
0634: int head_width, boolean thick) {
0635: if (thick) {
0636: drawArrowToBox(g, x1, y1, x2, y2, wHalfBox, hHalfBox,
0637: head_length, head_width, false);
0638: drawArrowToBox(g, x1 - 1, y1, x2 - 1, y2, wHalfBox,
0639: hHalfBox, head_length, head_width, false);
0640: drawArrowToBox(g, x1, y1 - 1, x2, y2 - 1, wHalfBox,
0641: hHalfBox, head_length, head_width, false);
0642: drawArrowToBox(g, x1 - 1, y1 - 1, x2 - 1, y2 - 1, wHalfBox,
0643: hHalfBox, head_length, head_width, false);
0644: } else {
0645: double dx = x2 - x1;
0646: double dy = y2 - y1;
0647: double d = Math.sqrt(dx * dx + dy * dy);
0648: if (d < 1.0) {
0649: d = 1.0;
0650: dx = 1;
0651: }
0652: dx /= d;
0653: dy /= d;
0654:
0655: double lx = head_length * dx;
0656: double ly = head_length * dy;
0657: double wx = head_width * dx;
0658: double wy = head_width * dy;
0659:
0660: double cp1 = dx * hHalfBox - dy * wHalfBox;
0661: double cp2 = dx * hHalfBox + dy * wHalfBox;
0662:
0663: if (cp1 < 0) {
0664: if (cp2 < 0) {
0665: // region I
0666: x2 += wHalfBox;
0667: y2 += wHalfBox * dy / dx;
0668: } else {
0669: // region II
0670: y2 -= hHalfBox;
0671: x2 -= hHalfBox * dx / dy;
0672: }
0673: } else {
0674: if (cp2 > 0) {
0675: // region III
0676: x2 -= wHalfBox;
0677: y2 -= wHalfBox * dy / dx;
0678: } else {
0679: // region IV
0680: y2 += hHalfBox;
0681: x2 += hHalfBox * dx / dy;
0682: }
0683: }
0684:
0685: g.drawLine(x1, y1, x2, y2);
0686: g.drawLine(x2, y2, (int) (x2 - lx + wy + 0.5), (int) (y2
0687: - ly - wx + .5));
0688: g.drawLine(x2, y2, (int) (x2 - lx - wy + 0.5), (int) (y2
0689: - ly + wx + .5));
0690: }
0691: }
0692:
0693: public synchronized FontMetrics getFontMetrics() {
0694: if (fm == null) {
0695: Dimension d = size();
0696: createOffscreenArea(d);
0697: }
0698: return fm;
0699: }
0700:
0701: // intercept font settings and transfer to offscreen buffer
0702: public synchronized void setFont(Font f) {
0703: super .setFont(f);
0704: if (offg != null) {
0705: offg.setFont(f);
0706: fm = offg.getFontMetrics();
0707: }
0708: }
0709:
0710: /*
0711: public Dimension preferredSize () {
0712: return new Dimension (400, 400);
0713: }
0714: */
0715:
0716: /*
0717: * Selecting and dragging nodes
0718: *
0719: */
0720:
0721: RenderedNode dragNode = null; // node being dragged, or null
0722: int dragOffsetX, dragOffsetY;
0723:
0724: // initial displacement of mouse cursor from dragged object's origin;
0725: // the object remains at this displacement throughout the drag
0726:
0727: void point(int x, int y) {
0728: Object over = pick(x, y);
0729: if (over == null) {
0730: if (tipObject != null || tip != null) {
0731: tipObject = null;
0732: tip = null;
0733: super .repaint();
0734: }
0735: } else if (over != tipObject) {
0736: String[] tipLines = ((Tipped) over).getTip();
0737:
0738: if (tipLines == null) {
0739: tipObject = null;
0740: tip = null;
0741: super .repaint();
0742: } else {
0743: tipObject = over;
0744: tip = new MultiLineString(tipLines);
0745: tipWidth = tip.getWidth(fm) + 10;
0746: tipHeight = tip.getHeight(fm) + 4;
0747: tipX = Math.max(x - tipWidth / 2, 0);
0748: tipY = Math.min(y + 25, offSize.height - tipHeight);
0749: super .repaint();
0750: }
0751: }
0752: }
0753:
0754: void leave() {
0755: if (tipObject != null || tip != null) {
0756: tip = null;
0757: tipObject = null;
0758: super .repaint();
0759: }
0760: }
0761:
0762: void click(int x, int y, boolean rightClick) {
0763: requestFocus();
0764:
0765: Object over = pick(x, y);
0766: if (over != null) {
0767: if (over instanceof RenderedNode) {
0768: RenderedNode n = (RenderedNode) over;
0769: //selectedNode = (RenderedNode)over;
0770: //selectedEdge = null;
0771:
0772: // start dragging the node
0773: if (!n.fixed) {
0774: dragNode = n;
0775: dragNode.fixed = true;
0776: dragOffsetX = (int) dragNode.screenX - x;
0777: dragOffsetY = (int) dragNode.screenY - y;
0778: }
0779: }
0780: //else {
0781: // // over instanceof RenderedEdge
0782: // selectedNode = null;
0783: // selectedEdge = (RenderedEdge)over;
0784: //}
0785: } else if (rightClick) {
0786: // right-click over background
0787: showControlPanel();
0788: }
0789: }
0790:
0791: void drag(int x, int y) {
0792: if (dragNode != null) {
0793: placeNodeOnScreen(dragNode, x + dragOffsetX, y
0794: + dragOffsetY);
0795: changedGraph();
0796: }
0797: }
0798:
0799: void drop(int x, int y) {
0800: if (dragNode != null) {
0801: placeNodeOnScreen(dragNode, x + dragOffsetX, y
0802: + dragOffsetY);
0803: changedGraph();
0804: dragNode.fixed = false;
0805: dragNode = null;
0806: }
0807: }
0808:
0809: public boolean handleEvent(Event event) {
0810: switch (event.id) {
0811: case Event.MOUSE_DOWN:
0812: click(event.x, event.y, event.metaDown());
0813: return true;
0814: case Event.MOUSE_UP:
0815: drop(event.x, event.y);
0816: return true;
0817: case Event.MOUSE_MOVE:
0818: point(event.x, event.y);
0819: return true;
0820: case Event.MOUSE_EXIT:
0821: leave();
0822: return true;
0823: case Event.MOUSE_DRAG:
0824: if (dragNode != null) {
0825: drag(event.x, event.y);
0826: return true;
0827: } else
0828: super .handleEvent(event);
0829: default:
0830: return super .handleEvent(event);
0831: }
0832: }
0833:
0834: /**
0835: * Find the object (Node or Edge) at position (x,y) relative to the window.
0836: * @param x X position
0837: * @param y Y position
0838: * @return topmost object under (x,y), or null if none
0839: */
0840: public Object pick(int x, int y) {
0841: // proceed in reverse display order: nodes first, then edges
0842: for (int i = graph.sizeNodes - 1; i >= 0; --i) {
0843: RenderedNode n = (RenderedNode) graph.nodes[i];
0844: if (Math.abs(n.screenX - x) < n.width / 2
0845: && Math.abs(n.screenY - y) < n.height / 2)
0846: return n;
0847: }
0848:
0849: for (int i = graph.sizeEdges - 1; i >= 0; --i) {
0850: RenderedEdge e = (RenderedEdge) graph.edges[i];
0851: RenderedNode to = (RenderedNode) e.to;
0852: RenderedNode from = (RenderedNode) e.from;
0853: if (inLineSegment(x, y, (int) to.screenX, (int) to.screenY,
0854: (int) from.screenX, (int) from.screenY, 4))
0855: return e;
0856: }
0857:
0858: return null;
0859: }
0860:
0861: boolean inLineSegment(int x, int y, int x1, int y1, int x2, int y2,
0862: int threshold) {
0863: int left, right, top, bottom;
0864: if (x1 < x2) {
0865: left = x1;
0866: right = x2;
0867: } else {
0868: left = x2;
0869: right = x1;
0870: }
0871: if (y1 < y2) {
0872: top = y1;
0873: bottom = y2;
0874: } else {
0875: top = y2;
0876: bottom = y1;
0877: }
0878:
0879: // check bounding box first
0880: if (x < left - threshold || x > right + threshold
0881: || y < top - threshold || y > bottom + threshold) {
0882: return false;
0883: }
0884:
0885: // equation for line is ax + by + c = 0
0886: // d/sqrt(a^2+b^2) is the distance between line and point <x,y>
0887: int a = y1 - y2;
0888: int b = x2 - x1;
0889: int c = x1 * y2 - x2 * y1;
0890: int d = a * x + b * y + c;
0891:
0892: return (d * d <= threshold * threshold * (a * a + b * b));
0893: }
0894:
0895: /*
0896: * Testing
0897: *
0898: public static void main (String[] args) {
0899: Frame f = new Frame ();
0900: f.addWindowListener (new WindowAdapter () {
0901: public void windowClosing (WindowEvent event) {
0902: ((Frame)event.getSource()).dispose();
0903: }
0904: });
0905: f.setSize (100,100);
0906: f.setLayout (new BorderLayout ());
0907:
0908: GraphLayout g = new GraphLayout ();
0909: f.add ("Center", g);
0910: f.show ();
0911:
0912: Node last = null;
0913: for (int i=0; i<args.length; ++i) {
0914: try {
0915: Thread.sleep (200);
0916: } catch (InterruptedException e) {}
0917: RenderedNode n = new RenderedNode();
0918: n.name = args[i];
0919: g.addNode (n);
0920: g.graph.placeNode (n, 0, 0);
0921:
0922: if (last != null) {
0923: RenderedEdge e = new RenderedEdge (last, n);
0924: g.addEdge (e);
0925: }
0926:
0927: }
0928: }
0929: */
0930: }
0931:
0932: class GraphLayoutControlPanel extends ClosableFrame {
0933: GraphLayout gl;
0934:
0935: Checkbox automatic;
0936:
0937: Scrollbar threshold;
0938: Scrollbar restLength;
0939: Scrollbar springConstant;
0940: Scrollbar nodeCharge;
0941:
0942: TextField thresholdText;
0943: TextField restLengthText;
0944: TextField springConstantText;
0945: TextField nodeChargeText;
0946:
0947: public GraphLayoutControlPanel(GraphLayout graphLayout) {
0948: super ("Graph Layout Control Panel", true);
0949: gl = graphLayout;
0950:
0951: setLayout(new GridBagLayout());
0952: Constrain.add(this ,
0953: automatic = new Checkbox("Automatic layout"), Constrain
0954: .labelLike(0, 0, 2));
0955: automatic.setState(true);
0956: Constrain.add(this , new Label("Threshold:", Label.LEFT),
0957: Constrain.labelLike(0, 1));
0958: Constrain
0959: .add(this , thresholdText = new TextField(String
0960: .valueOf(gl.getThreshold())), Constrain
0961: .fieldLike(1, 1));
0962: Constrain.add(this , threshold = new Scrollbar(
0963: Scrollbar.HORIZONTAL, (int) gl.getThreshold(), 50, 0,
0964: 1000), Constrain.fieldLike(0, 2, 2));
0965: Constrain.add(this , new Label("Rest length:", Label.LEFT),
0966: Constrain.labelLike(0, 3));
0967: Constrain.add(this , restLengthText = new TextField(String
0968: .valueOf(gl.getRestLength())), Constrain
0969: .fieldLike(1, 3));
0970: Constrain.add(this , restLength = new Scrollbar(
0971: Scrollbar.HORIZONTAL, (int) gl.getRestLength(), 50, 0,
0972: 1000), Constrain.fieldLike(0, 4, 2));
0973: Constrain.add(this , new Label("Spring constant:", Label.LEFT),
0974: Constrain.labelLike(0, 5));
0975: Constrain.add(this , springConstantText = new TextField(String
0976: .valueOf(gl.getSpringConstant())), Constrain.fieldLike(
0977: 1, 5));
0978: Constrain.add(this , springConstant = new Scrollbar(
0979: Scrollbar.HORIZONTAL, (int) gl.getSpringConstant(), 50,
0980: 0, 1000), Constrain.fieldLike(0, 6, 2));
0981: Constrain.add(this , new Label("Node charge:", Label.LEFT),
0982: Constrain.labelLike(0, 7));
0983: Constrain.add(this , nodeChargeText = new TextField(String
0984: .valueOf(Math.sqrt(gl.getNodeCharge()))), Constrain
0985: .fieldLike(1, 7));
0986: Constrain.add(this , nodeCharge = new Scrollbar(
0987: Scrollbar.HORIZONTAL, (int) (Math.sqrt(gl
0988: .getNodeCharge())), 50, 0, 1000), Constrain
0989: .fieldLike(0, 8, 2));
0990: pack();
0991: }
0992:
0993: public boolean handleEvent(Event event) {
0994: // FIX: doesn't support text entry
0995: if (event.target == automatic)
0996: gl.setAutomaticLayout(automatic.getState());
0997: else if (event.target == threshold)
0998: gl.setThreshold(((Integer) event.arg).intValue());
0999: else if (event.target == restLength)
1000: gl.setRestLength(((Integer) event.arg).intValue());
1001: else if (event.target == springConstant)
1002: gl.setSpringConstant(((Integer) event.arg).intValue());
1003: else if (event.target == restLength) {
1004: int v = ((Integer) event.arg).intValue();
1005: gl.setNodeCharge(v * v);
1006: } else
1007: return super .handleEvent(event);
1008: return true;
1009: }
1010: }
|