import java.util.*; import java.awt.*; import java.applet.Applet; import java.applet.AppletContext; import java.net.URL; import java.net.MalformedURLException; interface DisplayPage { public void displayPage (String strPage); } class Node { double x; double y; double dx; double dy; // NW: added size attributes double width; double height; // NW: added the 'fixed_neighbor' attribute boolean fixed_neighbor; boolean fixed; String lbl; } class Edge { int from; int to; double len; } class GraphPanel extends Panel implements Runnable { Graph graph; int nnodes; Node nodes[] = new Node[100]; int nedges; Edge edges[] = new Edge[200]; Node pick; boolean pickfixed; Thread relaxer; boolean stress; boolean random; DisplayPage pageDisplayer; GraphPanel(Graph graph) { this.graph = graph; } public void setPageDisplayer (DisplayPage displayer) { pageDisplayer = displayer; } int findNode(String lbl) { for (int i = 0 ; i < nnodes ; i++) { if (nodes[i].lbl.equals(lbl)) { return i; } } return addNode(lbl); } int addNode(String lbl) { Node n = new Node(); n.x = 10 + 380*Math.random(); n.y = 10 + 380*Math.random(); n.lbl = lbl; nodes[nnodes] = n; return nnodes++; } void addEdge(String from, String to, int len) { Edge e = new Edge(); e.from = findNode(from); e.to = findNode(to); e.len = len; edges[nedges++] = e; } public void run() { while (true) { relax(); repaint(); if (random && (Math.random() < 0.03)) { Node n = nodes[(int)(Math.random() * nnodes)]; if (!n.fixed) { n.x += 100*Math.random() - 50; n.y += 100*Math.random() - 50; } // graph.play(graph.getCodeBase(), "audio/drip.au"); } try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } synchronized void relax() { // each edge exerts a pull on its nodes for (int i = 0 ; i < nedges ; i++) { Edge e = edges[i]; // compute the length double vx = nodes[e.to].x - nodes[e.from].x; double vy = nodes[e.to].y - nodes[e.from].y; double len = Math.sqrt(vx * vx + vy * vy); // compute the x and y forces double f = (edges[i].len - len) / (len * 3) ; // NW: reduce the pull if there's a selected node if (pick != null) { f = f / 5; } else { f = f / 4; } double dx = f * vx; double dy = f * vy; // move the nodes nodes[e.to].dx += dx; nodes[e.to].dy += dy; nodes[e.from].dx += -dx; nodes[e.from].dy += -dy; } // node-node repulsion // the force on each node is the square root of the sum of the // repulsions of other nodes for (int i = 0 ; i < nnodes ; i++) { Node n1 = nodes[i]; double dx = 0; double dy = 0; for (int j = 0 ; j < nnodes ; j++) { if (i == j) { continue; } Node n2 = nodes[j]; double vx = n1.x - n2.x; double vy = n1.y - n2.y; double len = vx * vx + vy * vy; // NW: increase the repulsion for nodes that neighbor the // selected node if (n1.fixed_neighbor || n2.fixed_neighbor) { len = len / 1000; } // NW: this approach slows things down too much // // for (int e = 0; e < nedges ; e++) { // if ((edges[e].from == i && // nodes[edges[e].to].fixed) || // (edges[e].to == i && // nodes[edges[e].from].fixed)) { // len = len / 5; // } // } if (len == 0) { dx += Math.random(); dy += Math.random(); } else if (len < 100*100) { dx += vx / len; dy += vy / len; } } double dlen = dx * dx + dy * dy; if (dlen > 0) { dlen = Math.sqrt(dlen) / 2; n1.dx += dx / dlen; n1.dy += dy / dlen; } } // keep nodes within bounds Dimension d = size(); for (int i = 0 ; i < nnodes ; i++) { Node n = nodes[i]; if (!n.fixed) { n.x += Math.max(-5, Math.min(5, n.dx)); n.y += Math.max(-5, Math.min(5, n.dy)); //System.out.println("v= " + n.dx + "," + n.dy); // NW: added node width to computations if ((n.x - (n.width / 2)) < 0) { n.x = n.width / 2; } else if ((n.x + (n.width / 2)) > d.width) { n.x = d.width - (n.width / 2); } // NW: added node height to computations, too if ((n.y - (n.height / 2)) < 0) { n.y = n.height / 2; } else if ((n.y + (n.height / 2)) > d.height) { n.y = d.height - (n.height / 2); } } n.dx /= 2; n.dy /= 2; } } Image offscreen; Dimension offscreensize; Graphics offgraphics; final Color fixedColor = Color.red; final Color selectColor = Color.pink; final Color edgeColor = Color.black; final Color nodeColor = new Color(250, 220, 100); final Color stressColor = Color.gray; // NW: changed arcColor1 from black to white final Color arcColor1 = Color.white; final Color arcColor2 = Color.pink; final Color arcColor3 = Color.red; public void paintNode(Graphics g, Node n, FontMetrics fm) { String label = n.lbl; // NW: nodes change color when selected Color thisNodeColor; int x = (int)n.x; int y = (int)n.y; if ((n == pick) || (n.fixed) || (n.fixed_neighbor)) { thisNodeColor = fixedColor; } else { thisNodeColor = nodeColor; } g.setColor (thisNodeColor); // NW: the original code // g.setColor((n == pick) ? selectColor : (n.fixed ? fixedColor : nodeColor)); int w = fm.stringWidth(label) + 10; int h = fm.getHeight() + 4; g.fillRect(x - w/2, y - h / 2, w, h); g.setColor(Color.black); g.drawRect(x - w/2, y - h / 2, w-1, h-1); g.drawString(label, x - (w-10)/2, (y - (h-4)/2) + fm.getAscent()); n.width = w; n.height = h; } public synchronized void update(Graphics g) { Dimension d = size(); if ((offscreen == null) || (d.width != offscreensize.width) || (d.height != offscreensize.height)) { offscreen = createImage(d.width, d.height); offscreensize = d; offgraphics = offscreen.getGraphics(); offgraphics.setFont(getFont()); } // NW hard-coded color because 'getbackround()' doesn't do it right offgraphics.setColor(Color.black); // getBackground()); offgraphics.fillRect(0, 0, d.width, d.height); for (int i = 0 ; i < nedges ; i++) { Edge e = edges[i]; int x1 = (int)nodes[e.from].x; int y1 = (int)nodes[e.from].y; int x2 = (int)nodes[e.to].x; int y2 = (int)nodes[e.to].y; int len = (int)Math.abs(Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)) - e.len); offgraphics.setColor((len < 10) ? arcColor1 : (len < 20 ? arcColor2 : arcColor3)) ; offgraphics.drawLine(x1, y1, x2, y2); if (stress) { String lbl = String.valueOf(len); offgraphics.setColor(stressColor); offgraphics.drawString(lbl, x1 + (x2-x1)/2, y1 + (y2-y1)/2); offgraphics.setColor(edgeColor); } } FontMetrics fm = offgraphics.getFontMetrics(); for (int i = 0 ; i < nnodes ; i++) { paintNode(offgraphics, nodes[i], fm); } g.drawImage(offscreen, 0, 0, null); } public synchronized boolean mouseDown(Event evt, int x, int y) { double bestdist = Double.MAX_VALUE; int picked = 0; for (int i = 0 ; i < nnodes ; i++) { Node n = nodes[i]; double dist = (n.x - x) * (n.x - x) + (n.y - y) * (n.y - y); if (dist < bestdist) { pick = n; picked = i; bestdist = dist; } } // mark this node as 'fixed' pickfixed = pick.fixed; pick.fixed = true; // mark this node's neighbors accordingly for (int e = 0; e < nedges ; e++) { if (edges[e].from == picked) { nodes[edges[e].to].fixed_neighbor = true; } if (edges[e].to == picked) { nodes[edges[e].from].fixed_neighbor = true; } } // center the node under the mouse pick.x = x; pick.y = y; repaint(); // load the corresponding HTML file pageDisplayer.displayPage (pick.lbl); return true; } public synchronized boolean mouseDrag(Event evt, int x, int y) { pick.x = x; pick.y = y; repaint(); return true; } public synchronized boolean mouseUp(Event evt, int x, int y) { pick.x = x; pick.y = y; // un-fix this node pick.fixed = pickfixed; // un-fix the neighbors for (int e = 0; e < nedges ; e++) { if (nodes[edges[e].from] == pick) { nodes[edges[e].to].fixed_neighbor = false; } if (nodes[edges[e].to] == pick) { nodes[edges[e].from].fixed_neighbor = false; } } pick = null; repaint(); return true; } public void start() { relaxer = new Thread(this); relaxer.start(); } public void stop() { relaxer.stop(); } } public class Graph extends Applet implements DisplayPage { GraphPanel panel; String strUrlBase; String strTargetFrame; public void init() { setLayout(new BorderLayout()); panel = new GraphPanel(this); // NW: let the graph panel know how to notify the applet of a pick panel.setPageDisplayer (this); // NW: know from where to load pages, and where to display them strUrlBase = getParameter ("urlbase"); strTargetFrame = getParameter ("targetframe"); add("Center", panel); Panel p = new Panel(); // NW: removed the panel // add("South", p); p.add(new Button("Scramble")); p.add(new Button("Shake")); p.add(new Checkbox("Stress")); p.add(new Checkbox("Random")); String edges = getParameter("edges"); for (StringTokenizer t = new StringTokenizer(edges, ",") ; t.hasMoreTokens() ; ) { String str = t.nextToken(); int i = str.indexOf('-'); if (i > 0) { int len = 50; int j = str.indexOf('/'); if (j > 0) { len = Integer.valueOf(str.substring(j+1)).intValue(); str = str.substring(0, j); } panel.addEdge(str.substring(0,i), str.substring(i+1), len); } } Dimension d = size(); String center = getParameter("center"); if (center != null){ Node n = panel.nodes[panel.findNode(center)]; n.x = d.width / 2; n.y = d.height / 2; n.fixed = true; } } public void start() { panel.start(); } public void stop() { panel.stop(); } // NW: load a page in reponse to a call from the graph panel public void displayPage (String strPage) { showStatus (strPage); try { AppletContext context; String strURL = new String (strUrlBase + strPage); URL url = new URL (strURL); // not such a great idea after all // showStatus (strURL); context = getAppletContext(); context.showDocument (url, strTargetFrame); } catch (MalformedURLException e) { } } public boolean action(Event evt, Object arg) { if (arg instanceof Boolean) { if (((Checkbox)evt.target).getLabel().equals("Stress")) { panel.stress = ((Boolean)arg).booleanValue(); } else { panel.random = ((Boolean)arg).booleanValue(); } return true; } if ("Scramble".equals(arg)) { play(getCodeBase(), "audio/computer.au"); Dimension d = size(); for (int i = 0 ; i < panel.nnodes ; i++) { Node n = panel.nodes[i]; if (!n.fixed) { n.x = 10 + (d.width-20)*Math.random(); n.y = 10 + (d.height-20)*Math.random(); } } return true; } if ("Shake".equals(arg)) { play(getCodeBase(), "audio/gong.au"); Dimension d = size(); for (int i = 0 ; i < panel.nnodes ; i++) { Node n = panel.nodes[i]; if (!n.fixed) { n.x += 80*Math.random() - 40; n.y += 80*Math.random() - 40; } } return true; } return false; } }