/* * Self-Organizing Graphs and * ISOM Graph Layout Demonstration applet * * Copyright (c) 1998 Bernd Meyer * bernd.meyer@acm.org * http://www.bouce.to/BerndMeyer * * Sorry, the source is quite messy... * I wouldn't try to learn Java from this, * it is my second Java applet and was written in a hurry. * kind of rapid evolutionary prototyping, that is :-)) * * * To be changed: * - at some point allnodes was changed from array to vector. * in many places it is still handled like an array. * - Code is slow because too many intermediate objects are generated. * change to slots */ import java.util.*; import java.awt.*; import java.net.*; import java.io.*; import java.applet.Applet; import java.util.Vector; class Node { double x; double y; double old_x; double old_y; boolean fixed; String lbl; Vector succ; boolean visited; int distance; void update(double dx, double dy) { old_x=x; old_y=y; x += dx; y += dy; } } class IsomPanel extends Panel implements Runnable { Isom graph; int nnodes; Vector allnodes = new Vector(); Node winner; Vector queue=new Vector(); double last_x; double last_y; int min_x, max_x, min_y, max_y, first_x, first_y; int max_epochs; int epoch; int radius_constant_time; int radius; int min_radius; double adaption; double initial_adaption; double min_adaption; double factor; double cooling_factor; double temp; int jump_radius; int initial_jump_radius; int delay; TextField inputField=new TextField(30); Thread adjuster; boolean random=false; boolean trace=false; boolean clipping=false; boolean taboo=false; int tabooDist=60; static final int rectangular=0; static final int triangular=1; static final int circular=2; int distribution; static final int drag=1; static final int create_node=2; static final int create_edge1=3; static final int create_edge2=4; static final int delete_node=5; static final int define_area=6; static final int delete_edge1=7; static final int delete_edge2=8; int mode=drag; boolean stopped=true; IsomPanel(Isom graph) { this.graph = graph; } void initialize() { distribution=graph.controls._distribution.getSelectedIndex(); max_epochs=graph.controls._sliderMaxE.GetValue(); radius=graph.controls._sliderMaxR.GetValue(); min_radius=graph.controls._sliderMinR.GetValue(); radius_constant_time=graph.controls._sliderRCTime.GetValue(); initial_adaption=((double)graph.controls._sliderMaxA.GetValue()) / 100; adaption=initial_adaption; min_adaption=((double)graph.controls._sliderMinA.GetValue()) / 100; cooling_factor=((double)graph.controls._sliderCF.GetValue()) / 10; temp=((double)graph.controls._sliderTemp.GetValue()) / 100; initial_jump_radius=graph.controls._sliderJump.GetValue(); jump_radius=initial_jump_radius; tabooDist=graph.controls._sliderTaboo.GetValue(); delay=graph.controls._sliderDel.GetValue(); pick=null; last_x=0; last_y=0; min_x=10; min_y=10; max_x=size().width-20; max_y=size().height-20; winner=null; for (int i = 0 ; i < nnodes ; i++) { ((Node)allnodes.elementAt(i)).visited = false; } epoch=1; repaint(); } String parameterString() { return "Distribution "+distribution + "\n" + "MaxEpochs "+max_epochs + "\n" + "MaxRadius "+radius + "\n" + "MinRadius "+min_radius + "\n" + "RCTime "+radius_constant_time + "\n" + "MaxAdaption "+adaption + "\n" + "MinAdaption "+min_adaption + "\n" + "CoolingFactor "+cooling_factor + "\n" + "Temperature "+temp + "\n" + "JumpRadius "+jump_radius + "\n"+ "Randomize " + random + "\n" + "Clipping " + clipping + "\n" + "TabooSize " + tabooDist + "\n" + "Taboo " + taboo; } void printParameters() { initialize(); System.out.println(parameterString()); } String graphDescr() { String output="NumberOfNodes "+allnodes.size()+"\n"; int nEdges=0; Enumeration nodes, edges; Node n1, n2; nodes=allnodes.elements(); while (nodes.hasMoreElements()) { n1 = (Node)nodes.nextElement(); output += n1.lbl+":"+n1.x+"/"+n1.y+"\n"; nEdges += n1.succ.size(); // count all Edges } // now print all (even reduandant) edges // redundant edges will be eliminated upon insert output += "NumberOfEdges "+nEdges; nodes=allnodes.elements(); while (nodes.hasMoreElements()) { n1 = (Node)nodes.nextElement(); edges = n1.succ.elements(); while (edges.hasMoreElements()) { n2 = (Node)edges.nextElement(); output += "\n"+n1.lbl+"-"+n2.lbl; } } return output; } void printGraph() { System.out.println(graphDescr()); } void loadGraph() { nnodes=0; allnodes.removeAllElements(); URL source=null; String line, input=""; try { source=new URL(graph.getDocumentBase(),inputField.getText()); } catch(MalformedURLException e) { System.out.println("Malformed URL!"); } System.out.println("Now reading from URL:"+source); try { URLConnection conn = source.openConnection(); DataInputStream inp = new DataInputStream(conn.getInputStream()); while((line = inp.readLine()) != null) { input = input + "\n" + line; } inp.close(); loadInitialize(input); } catch(Exception e) { System.out.println("Can't read file from URL."); } } void loadInitialize(String input) { StringTokenizer t = new StringTokenizer(input, ":/- \t\n\r"); try { t.nextToken(); graph.controls._distribution.select(Integer.parseInt(t.nextToken())); t.nextToken(); graph.controls._sliderMaxE.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); graph.controls._sliderMaxR.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); graph.controls._sliderMinR.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); graph.controls._sliderRCTime.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); double val = new Double(t.nextToken()).doubleValue()*100; graph.controls._sliderMaxA.SetValue((int)val); t.nextToken(); val = new Double(t.nextToken()).doubleValue()*100; graph.controls._sliderMinA.SetValue((int)val); t.nextToken(); val = new Double(t.nextToken()).doubleValue()*10; graph.controls._sliderCF.SetValue((int)val); t.nextToken(); val = new Double(t.nextToken()).doubleValue()*100; graph.controls._sliderTemp.SetValue((int)val); t.nextToken(); graph.controls._sliderJump.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); random = new Boolean(t.nextToken()).booleanValue(); graph.randomBox.setState(random); t.nextToken(); clipping = new Boolean(t.nextToken()).booleanValue(); graph.clipBox.setState(clipping); t.nextToken(); graph.controls._sliderTaboo.SetValue(Integer.parseInt(t.nextToken())); t.nextToken(); taboo = new Boolean(t.nextToken()).booleanValue(); graph.tabooBox.setState(taboo); t.nextToken(); int numNodes=Integer.parseInt(t.nextToken()); for (int i=1; i<=numNodes; i++) { String lbl=t.nextToken(); double xPos=new Double(t.nextToken()).doubleValue(); double yPos=new Double(t.nextToken()).doubleValue(); addNode(lbl, xPos, yPos); } t.nextToken(); int numEdges=Integer.parseInt(t.nextToken()); for (int i=1; i<=numEdges; i++) { addEdge(t.nextToken(),t.nextToken()); } initialize(); } catch (Exception e) { System.out.println("Input File not format compliant."); } } void scramble() { Dimension d = size(); for (int i = 0 ; i < nnodes ; i++) { Node n = (Node) allnodes.elementAt(i); if (!n.fixed) { n.x = 10 + (d.width-20)*Math.random(); n.y = 10 + (d.height-20)*Math.random(); } } } void shake() { Dimension d = size(); for (int i = 0 ; i < nnodes ; i++) { Node n = (Node) allnodes.elementAt(i); if (!n.fixed) { n.x += initial_jump_radius*Math.random() -initial_jump_radius/2; n.y += initial_jump_radius*Math.random() -initial_jump_radius/2; } } } int findNode(String lbl) { for (int i = 0 ; i < nnodes ; i++) { if (((Node)allnodes.elementAt(i)).lbl.equals(lbl)) { return i; } } return addNode(lbl); } Node closestNode(double x, double y) { double dx, dy, x0, y0, dist; double d=Double.MAX_VALUE; Node current=null; for (int i = 0 ; i < nnodes ; i++) { x0=((Node)allnodes.elementAt(i)).x; y0=((Node)allnodes.elementAt(i)).y; if (inside_distribution(x0,y0)) { dx=x0-x; dy=y0-y; dist=Math.sqrt(dx*dx+dy*dy); if (d>dist) { d = dist; current=((Node)allnodes.elementAt(i)); } } } return current; } void random_vector() { last_x=min_x + (max_x-min_x)*Math.random(); last_y=min_y + (max_y-min_y)*Math.random(); } int addNode(String lbl) { Node n = new Node(); random_vector(); n.x = last_x; n.y = last_y; n.lbl = lbl; n.succ=new Vector(); allnodes.addElement(n); return nnodes++; } int addNode(String lbl, double x, double y) { int n=addNode(lbl); ((Node)allnodes.lastElement()).x=x; ((Node)allnodes.lastElement()).y=y; return n; } void addEdge(String from, String to) { Node fromNode = (Node) allnodes.elementAt(findNode(from)); Node toNode = (Node) allnodes.elementAt(findNode(to)); if (!fromNode.succ.contains(toNode)) { fromNode.succ.addElement(toNode); } // ...and since we are currently handling undirected edges only... if (!toNode.succ.contains(fromNode)) { toNode.succ.addElement(fromNode); } } public void run() { while (true) { if (!stopped && max_epochs>epoch) { adjust(); update_parameters(); if (trace) { System.out.println("epoch="+epoch+", "+ "adation="+adaption+", "+ "radius="+radius); } } else if (!stopped && max_epochs==epoch) { stopped=true; } repaint(); if (random && (Math.random() < temp)) { Node n = (Node) allnodes.elementAt((int)(Math.random() * nnodes)); if (!n.fixed) { n.x += jump_radius*Math.random() - jump_radius/2; n.y += jump_radius*Math.random() - jump_radius/2; } } try { Thread.sleep(delay); } catch (InterruptedException e) { break; } } } synchronized void update_parameters() { epoch += 1; // linear cooling: // factor=(1-(double)epoch/max_epochs); // negative exponential cooling: factor=Math.exp(-1*cooling_factor*(double)epoch/max_epochs); adaption=Math.max(min_adaption, factor*initial_adaption); jump_radius=(int)factor*jump_radius; temp=factor*temp; // apparently it is not a good idea to update just a single node // in isolation (radius=0) if ((radius > min_radius) && (0==epoch%radius_constant_time)) { radius -= 1; } } synchronized void adjust() { random_input(); winner = closestNode(last_x,last_y); for (int i = 0 ; i < nnodes ; i++) { ((Node)allnodes.elementAt(i)).visited = false; ((Node)allnodes.elementAt(i)).distance=0; } adjustNodes(winner); } boolean inside_triangle(double x, double y) { double range=(y-min_y)/(max_y-min_y)*(max_x-min_x)/2; return (x>=(min_x+(max_x-min_x)/2-range) && x<=(min_x+(max_x-min_x)/2+range)); } boolean inside_circle(double x, double y) { double centerX=min_x+(max_x-min_x)/2; double centerY=min_y+(max_y-min_y)/2; double radius=Math.min(max_x-min_x,max_y-min_y)/2; double dist= Math.sqrt(Math.pow((centerX-x),2.0)+Math.pow((centerY-y),2.0)); return (dist<=radius); } synchronized void random_input() { // here is an inefficient way to ensure equal // distribution over selected area random_vector(); while (!inside_distribution(last_x,last_y)) random_vector(); } boolean inside_distribution(double x, double y) { switch (distribution) { case rectangular: return (x>=min_x && x<=max_x && y>=min_y && y<=max_y); case triangular: return inside_triangle(x,y); case circular: return inside_circle(x,y); default: last_x=0; last_y=0; return true; } } synchronized void adjustNodes(Node n) { // we have to do a breadth first traversal here queue.removeAllElements(); n.distance=0; n.visited=true; queue.addElement(n); Node current; while (!queue.isEmpty()) { current=(Node)queue.elementAt(0); queue.removeElementAt(0); double dx=last_x-current.x; double dy=last_y-current.y; double factor=adaption/Math.pow(2,current.distance); current.update(factor*dx, factor*dy); /* System.out.println("Adjusting node "+current.lbl+"@d="+ current.distance+": dx="+ factor*dx+" and dy="+factor*dy+"."); */ if (current.distance 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)); } } panel.last_x=0; panel.last_y=0; Dimension d = size(); String center = getParameter("center"); if (center != null){ Node n = (Node) panel.allnodes.elementAt(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(); } public boolean action(Event evt, Object arg) { if (arg instanceof Boolean) { String label=((Checkbox)evt.target).getLabel(); if (label.equals("Random Jumps")) panel.random = ((Boolean)arg).booleanValue(); if (label.equals("Trace")) panel.trace = ((Boolean)arg).booleanValue(); if (label.equals("Drag")) panel.mode=panel.drag; if (label.equals("+Edge")) panel.mode=panel.create_edge1; if (label.equals("-Edge")) panel.mode=panel.delete_edge1; if (label.equals("+Node")) panel.mode=panel.create_node; if (label.equals("-Node")) panel.mode=panel.delete_node; if (label.equals("Area")) panel.mode=panel.define_area; if (label.equals("Clip")) panel.clipping=((Boolean)arg).booleanValue(); if (label.equals("Taboo")) panel.taboo=((Boolean)arg).booleanValue(); return true; } if ("Scramble".equals(arg)) { panel.scramble(); return true; } if ("Shake".equals(arg)) { panel.shake(); return true; } if ("Controls".equals(arg)) { if( !controls.showing() ) { controls.start(); controls.showing( true ); } controls.show(); return true; } if ("Save".equals(arg)) { System.out.println("----> CUT BELOW <----"); panel.printParameters(); panel.printGraph(); System.out.println("----> CUT ABOVE <----"); } if ("Load".equals(arg)) { panel.loadGraph(); panel.repaint(); } if ("Start".equals(arg)) { panel.stopped=false; return true; } if ("Stop".equals(arg)) { panel.stopped=true; return true; } if ("Initialize".equals(arg)) { panel.initialize(); return true; } return false; } public String getAppletInfo() { return "Title: Isom-Graph Layout \nAuthor: Bernd Meyer (bernd.meyer@acm.org)"; } public String[][] getParameterInfo() { String[][] info = { {"edges", "delimited string", "A comma-delimited list of all the edges. It takes the form of 'C-N1,C-N2,C-N3,C-NX,N1-N2/M12,N2-N3/M23,N3-NX/M3X,...' where C is the name of center node (see 'center' parameter) and NX is a node attached to the center node. For the edges connecting nodes to eachother (and not to the center node) you may (optionally) specify a length MXY separated from the edge name by a forward slash."}, {"center", "string", "The name of the center node."} }; return info; } }