001/* 002 * Copyright 2006 - 2013 003 * Stefan Balev <stefan.balev@graphstream-project.org> 004 * Julien Baudry <julien.baudry@graphstream-project.org> 005 * Antoine Dutot <antoine.dutot@graphstream-project.org> 006 * Yoann Pigné <yoann.pigne@graphstream-project.org> 007 * Guilhelm Savin <guilhelm.savin@graphstream-project.org> 008 * 009 * This file is part of GraphStream <http://graphstream-project.org>. 010 * 011 * GraphStream is a library whose purpose is to handle static or dynamic 012 * graph, create them from scratch, file or any source and display them. 013 * 014 * This program is free software distributed under the terms of two licenses, the 015 * CeCILL-C license that fits European law, and the GNU Lesser General Public 016 * License. You can use, modify and/ or redistribute the software under the terms 017 * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following 018 * URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by 019 * the Free Software Foundation, either version 3 of the License, or (at your 020 * option) any later version. 021 * 022 * This program is distributed in the hope that it will be useful, but WITHOUT ANY 023 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 024 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 025 * 026 * You should have received a copy of the GNU Lesser General Public License 027 * along with this program. If not, see <http://www.gnu.org/licenses/>. 028 * 029 * The fact that you are presently reading this means that you have had 030 * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms. 031 */ 032package org.graphstream.ui.swingViewer; 033 034import java.awt.event.ActionEvent; 035import java.awt.event.ActionListener; 036import java.security.AccessControlException; 037import java.util.HashMap; 038 039import javax.swing.Timer; 040 041import org.graphstream.graph.Edge; 042import org.graphstream.graph.Graph; 043import org.graphstream.graph.Node; 044import org.graphstream.stream.ProxyPipe; 045import org.graphstream.stream.Source; 046import org.graphstream.stream.thread.ThreadProxyPipe; 047import org.graphstream.ui.geom.Point3; 048import org.graphstream.ui.graphicGraph.GraphicGraph; 049import org.graphstream.ui.layout.Layout; 050import org.graphstream.ui.layout.LayoutRunner; 051import org.graphstream.ui.layout.Layouts; 052import org.graphstream.ui.swingViewer.basicRenderer.SwingBasicGraphRenderer; 053 054/** 055 * Set of views on a graphic graph. 056 * 057 * <p> 058 * The viewer class is in charge of maintaining : 059 * <ul> 060 * <li>A "graphic graph" (a special graph that internally stores the graph under 061 * the form of style sets of "graphic" elements, suitable to draw the graph, but 062 * not to adapted to used it as a general graph),</li> 063 * <li>The eventual proxy pipe from which the events come from (but graph events 064 * can come from any kind of source),</li> 065 * <li>A default view, and eventually more views on the graphic graph.</li> 066 * <li>A flag that allows to repaint the view only if the graphic graph changed. 067 * <li> 068 * </ul> 069 * </p> 070 * 071 * <p> 072 * The graphic graph can be created by the viewer or given at construction (to 073 * share it with another viewer). 074 * </p> 075 * 076 * <p> 077 * <u>Once created, the viewer runs in a loop inside the Swing thread. You 078 * cannot call methods on it directly if you are not in this thread</u>. The 079 * only operation that you can use in other threads is the constructor, the 080 * {@link #addView(View)}, {@link #removeView(String)} and the {@link #close()} 081 * methods. Other methods are not protected from concurrent accesses. 082 * </p> 083 * 084 * <p> 085 * Some constructors allow a {@link ProxyPipe} as argument. If given, the 086 * graphic graph is made listener of this pipe and the pipe is "pumped" during 087 * the view loop. This allows to run algorithms on a graph in the main thread 088 * (or any other thread) while letting the viewer run in the swing thread. 089 * </p> 090 * 091 * <p> 092 * Be very careful: due to the nature of graph events in GraphStream, the viewer 093 * is not aware of events that occured on the graph <u>before</u> its creation. 094 * There is a special mechanism that replay the graph if you use a proxy pipe or 095 * if you pass the graph directly. However, when you create the viewer by 096 * yourself and only pass a {@link Source}, the viewer <u>will not</u> display 097 * the events that occured on the source before it is connected to it. 098 * </p> 099 */ 100public class Viewer implements ActionListener { 101 // Attributes 102 103 /** 104 * Name of the default view. 105 */ 106 public static String DEFAULT_VIEW_ID = "defaultView"; 107 108 /** 109 * What to do when a view frame is closed. 110 */ 111 public static enum CloseFramePolicy { 112 CLOSE_VIEWER, HIDE_ONLY, EXIT 113 }; 114 115 /** 116 * How does the viewer synchronise its internal graphic graph with the graph 117 * displayed. The graph we display can be in the Swing thread (as will be 118 * the viewer, therefore in the same thread as the viewer), in another 119 * thread, or on a distant machine. 120 */ 121 public enum ThreadingModel { 122 GRAPH_IN_SWING_THREAD, GRAPH_IN_ANOTHER_THREAD, GRAPH_ON_NETWORK 123 }; 124 125 // Attribute 126 127 /** 128 * If true the graph we display is in another thread, the synchronisation 129 * between the graph and the graphic graph must therefore use thread 130 * proxies. 131 */ 132 protected boolean graphInAnotherThread = true; 133 134 /** 135 * The graph observed by the views. 136 */ 137 protected GraphicGraph graph; 138 139 /** 140 * If we have to pump events by ourself. 141 */ 142 protected ProxyPipe pumpPipe; 143 144 /** 145 * If we take graph events from a source in this thread. 146 */ 147 protected Source sourceInSameThread; 148 149 /** 150 * Timer in the Swing thread. 151 */ 152 protected Timer timer; 153 154 /** 155 * Delay in milliseconds between frames. 156 */ 157 protected int delay = 40; 158 159 /** 160 * The set of views. 161 */ 162 protected HashMap<String, View> views = new HashMap<String, View>(); 163 164 /** 165 * What to do when a view frame is closed. 166 */ 167 protected CloseFramePolicy closeFramePolicy = CloseFramePolicy.EXIT; 168 169 // Attribute 170 171 /** 172 * Optional layout algorithm running in another thread. 173 */ 174 protected LayoutRunner optLayout = null; 175 176 /** 177 * If there is a layout in another thread, this is the pipe coming from it. 178 */ 179 protected ProxyPipe layoutPipeIn = null; 180 181 // Construction 182 183 /** 184 * The graph or source of graph events is in another thread or on another 185 * machine, but the pipe already exists. The graphic graph displayed by this 186 * viewer is created. 187 * 188 * @param source 189 * The source of graph events. 190 */ 191 public Viewer(ProxyPipe source) { 192 graphInAnotherThread = true; 193 init(new GraphicGraph(newGGId()), source, (Source) null); 194 } 195 196 /** 197 * We draw a pre-existing graphic graph. The graphic graph is maintained by 198 * its creator. 199 * 200 * @param graph 201 * THe graph to draw. 202 */ 203 public Viewer(GraphicGraph graph) { 204 graphInAnotherThread = false; 205 init(graph, (ProxyPipe) null, (Source) null); 206 } 207 208 /** 209 * New viewer on an existing graph. The viewer always run in the Swing 210 * thread, therefore, you must specify how it will take graph events from 211 * the graph you give. If the graph you give will be accessed only from the 212 * Swing thread use ThreadingModel.GRAPH_IN_SWING_THREAD. If the graph you 213 * use is accessed in another thread use 214 * ThreadingModel.GRAPH_IN_ANOTHER_THREAD. This last scheme is more powerful 215 * since it allows to run algorithms on the graph in parallel with the 216 * viewer. 217 * 218 * @param graph 219 * The graph to render. 220 * @param threadingModel 221 * The threading model. 222 */ 223 public Viewer(Graph graph, ThreadingModel threadingModel) { 224 switch (threadingModel) { 225 case GRAPH_IN_SWING_THREAD: 226 graphInAnotherThread = false; 227 init(new GraphicGraph(newGGId()), (ProxyPipe) null, graph); 228 enableXYZfeedback(true); 229 break; 230 case GRAPH_IN_ANOTHER_THREAD: 231 graphInAnotherThread = true; 232 233 ThreadProxyPipe tpp = new ThreadProxyPipe(); 234 tpp.init(graph, true); 235 236 init(new GraphicGraph(newGGId()), tpp, (Source) null); 237 enableXYZfeedback(false); 238 break; 239 case GRAPH_ON_NETWORK: 240 throw new RuntimeException("TO DO, sorry !:-)"); 241 } 242 } 243 244 /** 245 * Create a new unique identifier for a graph. 246 * 247 * @return The new identifier. 248 */ 249 protected String newGGId() { 250 return String.format("GraphicGraph_%d", (int) (Math.random() * 10000)); 251 } 252 253 /** 254 * Initialise the viewer. 255 * 256 * @param graph 257 * The graphic graph. 258 * @param ppipe 259 * The source of events from another thread or machine (null if 260 * source != null). 261 * @param source 262 * The source of events from this thread (null if ppipe != null). 263 */ 264 protected void init(GraphicGraph graph, ProxyPipe ppipe, Source source) { 265 this.graph = graph; 266 this.pumpPipe = ppipe; 267 this.sourceInSameThread = source; 268 this.timer = new Timer(delay, this); 269 270 assert ((ppipe != null && source == null) || (ppipe == null && source != null)); 271 272 if (pumpPipe != null) 273 pumpPipe.addSink(graph); 274 if (sourceInSameThread != null) { 275 if (source instanceof Graph) 276 replayGraph((Graph) source); 277 sourceInSameThread.addSink(graph); 278 } 279 280 timer.setCoalesce(true); 281 timer.setRepeats(true); 282 timer.start(); 283 } 284 285 /** 286 * Close definitively this viewer and all its views. 287 */ 288 public void close() { 289 synchronized (views) { 290 disableAutoLayout(); 291 292 for (View view : views.values()) 293 view.close(graph); 294 295 timer.stop(); 296 timer.removeActionListener(this); 297 298 if (pumpPipe != null) 299 pumpPipe.removeSink(graph); 300 if (sourceInSameThread != null) 301 sourceInSameThread.removeSink(graph); 302 303 graph = null; 304 pumpPipe = null; 305 sourceInSameThread = null; 306 timer = null; 307 } 308 } 309 310 // Access 311 312 /** 313 * Create a new instance of the default graph renderer. The default graph 314 * renderer class is given by the "org.graphstream.ui.renderer" system 315 * property. If the class indicated by this property is not usable (not in 316 * the class path, not of the correct type, etc.) or if the property is not 317 * present a SwingBasicGraphRenderer is returned. 318 */ 319 public static GraphRenderer newGraphRenderer() { 320 String rendererClassName; 321 322 try { 323 rendererClassName = System.getProperty("gs.ui.renderer"); 324 325 if (rendererClassName != null) { 326 System.err.printf("\"gs.ui.renderer\" is deprecated,"); 327 System.err.printf("use \"org.graphstream.ui.renderer\"" 328 + " instead\n"); 329 } else { 330 rendererClassName = System 331 .getProperty("org.graphstream.ui.renderer"); 332 } 333 } catch (AccessControlException e) { 334 rendererClassName = null; 335 } 336 337 if (rendererClassName == null) 338 return new SwingBasicGraphRenderer(); 339 340 try { 341 Class<?> c = Class.forName(rendererClassName); 342 Object object = c.newInstance(); 343 344 if (object instanceof GraphRenderer) { 345 return (GraphRenderer) object; 346 } else { 347 System.err.printf("class '%s' is not a 'GraphRenderer'%n", 348 object); 349 } 350 } catch (ClassNotFoundException e) { 351 e.printStackTrace(); 352 System.err 353 .printf("Cannot create graph renderer, 'GraphRenderer' class not found : " 354 + e.getMessage()); 355 } catch (InstantiationException e) { 356 e.printStackTrace(); 357 System.err.printf("Cannot create graph renderer, class '" 358 + rendererClassName + "' error : " + e.getMessage()); 359 } catch (IllegalAccessException e) { 360 e.printStackTrace(); 361 System.err.printf("Cannot create graph renderer, class '" 362 + rendererClassName + "' illegal access : " 363 + e.getMessage()); 364 } 365 366 return new SwingBasicGraphRenderer(); 367 } 368 369 /** 370 * What to do when a frame is closed. 371 */ 372 public CloseFramePolicy getCloseFramePolicy() { 373 return closeFramePolicy; 374 } 375 376 /** 377 * New proxy pipe on events coming from the viewer through a thread. 378 * 379 * @return The new proxy pipe. 380 */ 381 public ProxyPipe newThreadProxyOnGraphicGraph() { 382 ThreadProxyPipe tpp = new ThreadProxyPipe(); 383 tpp.init(graph); 384 385 return tpp; 386 } 387 388 /** 389 * New viewer pipe on the events coming from the viewer through a thread. 390 * 391 * @return The new viewer pipe. 392 */ 393 public ViewerPipe newViewerPipe() { 394 ThreadProxyPipe tpp = new ThreadProxyPipe(); 395 tpp.init(graph, false); 396 397 enableXYZfeedback(true); 398 399 return new ViewerPipe(String.format("viewer_%d", 400 (int) (Math.random() * 10000)), tpp); 401 } 402 403 /** 404 * The underlying graphic graph. Caution : Use the returned graph only in 405 * the Swing thread !! 406 */ 407 public GraphicGraph getGraphicGraph() { 408 return graph; 409 } 410 411 /** 412 * The view that correspond to the given identifier. 413 * 414 * @param id 415 * The view identifier. 416 * @return A view or null if not found. 417 */ 418 public View getView(String id) { 419 synchronized (views) { 420 return views.get(id); 421 } 422 } 423 424 /** 425 * The default view. This is a shortcut to a call to 426 * {@link #getView(String)} with {@link #DEFAULT_VIEW_ID} as parameter. 427 * 428 * @return The default view or null if no default view has been installed. 429 */ 430 public View getDefaultView() { 431 return getView(DEFAULT_VIEW_ID); 432 } 433 434 // Command 435 436 /** 437 * Build the default graph view and insert it. The view identifier is 438 * {@link #DEFAULT_VIEW_ID}. You can request the view to be open in its own 439 * frame. 440 * 441 * @param openInAFrame 442 * It true, the view is placed in a frame, else the view is only 443 * created and you must embed it yourself in your application. 444 */ 445 public View addDefaultView(boolean openInAFrame) { 446 synchronized (views) { 447 View view = new DefaultView(this, DEFAULT_VIEW_ID, 448 newGraphRenderer()); 449 addView(view); 450 451 if (openInAFrame) 452 view.openInAFrame(true); 453 454 return view; 455 } 456 } 457 458 /** 459 * Add a view using its identifier. If there was already a view with this 460 * identifier, it is closed and returned (if different of the one added). 461 * 462 * @param view 463 * The view to add. 464 * @return The old view that was at the given identifier, if any, else null. 465 */ 466 public View addView(View view) { 467 synchronized (views) { 468 View old = views.put(view.getId(), view); 469 470 if (old != null && old != view) 471 old.close(graph); 472 473 return old; 474 } 475 } 476 477 /** 478 * Add a new default view with a specific renderer. If a view with the same 479 * id exists, it is removed and closed. By default the view is open in a 480 * frame. 481 * 482 * @param id 483 * The new view identifier. 484 * @param renderer 485 * The renderer to use. 486 * @return The created view. 487 */ 488 public View addView(String id, GraphRenderer renderer) { 489 return addView(id, renderer, true); 490 } 491 492 /** 493 * Same as {@link #addView(String, GraphRenderer)} but allows to specify 494 * that the view uses a frame or not. 495 * 496 * @param id 497 * The new view identifier. 498 * @param renderer 499 * The renderer to use. 500 * @param openInAFrame 501 * If true the view is open in a frame, else the returned view is 502 * a JPanel that can be inserted in a GUI. 503 * @return The created view. 504 */ 505 public View addView(String id, GraphRenderer renderer, boolean openInAFrame) { 506 synchronized (views) { 507 View view = new DefaultView(this, id, renderer); 508 addView(view); 509 510 if (openInAFrame) 511 view.openInAFrame(true); 512 513 return view; 514 } 515 } 516 517 /** 518 * Remove a view. The view is not closed. 519 * 520 * @param id 521 * The view identifier. 522 */ 523 public void removeView(String id) { 524 synchronized (views) { 525 views.remove(id); 526 } 527 } 528 529 /** 530 * Called on a regular basis by the timer. Checks if some events occurred 531 * from the graph pipe or from the layout pipe, and if the graph changed, 532 * triggers a repaint. Never call this method, it is called by a Swing Timer 533 * automatically. 534 */ 535 public void actionPerformed(ActionEvent e) { 536 synchronized (views) { 537 // long t1=System.currentTimeMillis(); 538 // long gsize1=graph.getNodeCount(); 539 if (pumpPipe != null) 540 pumpPipe.pump(); 541 // long gsize2=graph.getNodeCount(); 542 // long t2=System.currentTimeMillis(); 543 544 if (layoutPipeIn != null) 545 layoutPipeIn.pump(); 546 // long t3=System.currentTimeMillis(); 547 548 boolean changed = graph.graphChangedFlag(); 549 550 if (changed) { 551 computeGraphMetrics(); 552 // long t4=System.currentTimeMillis(); 553 554 for (View view : views.values()) 555 view.display(graph, changed); 556 } 557 // long t5=System.currentTimeMillis(); 558 559 graph.resetGraphChangedFlag(); 560 561 // System.err.printf("display pump=%f layoutPump=%f metrics=%f display=%f (size delta=%d size1=%d size2=%d)%n", 562 // (t2-t1)/1000.0, (t3-t2)/1000.0, (t4-t3)/1000.0, (t5-t4)/1000.0, 563 // (gsize2-gsize1), gsize1, gsize2); 564 } 565 } 566 567 /** 568 * Compute the overall bounds of the graphic graph according to the nodes 569 * and sprites positions. We can only compute the graph bounds from the 570 * nodes and sprites centres since the node and graph bounds may in certain 571 * circumstances be computed according to the graph bounds. The bounds are 572 * stored in the graph metrics. 573 */ 574 protected void computeGraphMetrics() { 575 graph.computeBounds(); 576 577 synchronized (views) { 578 Point3 lo = graph.getMinPos(); 579 Point3 hi = graph.getMaxPos(); 580 581 for (View view : views.values()) 582 view.getCamera().setBounds(lo.x, lo.y, lo.z, hi.x, hi.y, hi.z); 583 } 584 } 585 586 /** 587 * What to do when the frame containing one or more views is closed. 588 * 589 * @param policy 590 * The close frame policy. 591 */ 592 public void setCloseFramePolicy(CloseFramePolicy policy) { 593 synchronized (views) { 594 closeFramePolicy = policy; 595 } 596 } 597 598 // Optional layout algorithm 599 600 /** 601 * Enable or disable the "xyz" attribute change when a node is moved in the 602 * views. By default the "xyz" attribute is changed. 603 * 604 * By default, each time a node of the graphic graph is moved, its "xyz" 605 * attribute is reset to follow the node position. This is useful only if 606 * someone listen at the graphic graph or use the graphic graph directly. 607 * But this operation is quite costly. Therefore by default if this viewer 608 * runs in its own thread, and the main graph is in another thread, xyz 609 * attribute change will be disabled until a listener is added. 610 * 611 * When the viewer is created to be used only in the swing thread, this 612 * feature is always on. 613 */ 614 public void enableXYZfeedback(boolean on) { 615 synchronized (views) { 616 graph.feedbackXYZ(on); 617 } 618 } 619 620 /** 621 * Launch an automatic layout process that will position nodes in the 622 * background. 623 */ 624 public void enableAutoLayout() { 625 enableAutoLayout(Layouts.newLayoutAlgorithm()); 626 } 627 628 /** 629 * Launch an automatic layout process that will position nodes in the 630 * background. 631 * 632 * @param layoutAlgorithm 633 * The algorithm to use (see Layouts.newLayoutAlgorithm() for the 634 * default algorithm). 635 */ 636 public void enableAutoLayout(Layout layoutAlgorithm) { 637 synchronized (views) { 638 if (optLayout == null) { 639 // optLayout = new LayoutRunner(graph, layoutAlgorithm, true, 640 // true); 641 optLayout = new LayoutRunner(graph, layoutAlgorithm, true, 642 false); 643 graph.replay(); 644 layoutPipeIn = optLayout.newLayoutPipe(); 645 layoutPipeIn.addAttributeSink(graph); 646 } 647 } 648 } 649 650 /** 651 * Disable the running automatic layout process, if any. 652 */ 653 public void disableAutoLayout() { 654 synchronized (views) { 655 if (optLayout != null) { 656 ((ThreadProxyPipe) layoutPipeIn).unregisterFromSource(); 657 layoutPipeIn.removeSink(graph); 658 layoutPipeIn = null; 659 optLayout.release(); 660 optLayout = null; 661 } 662 } 663 } 664 665 /** Dirty replay of the graph. */ 666 protected void replayGraph(Graph graph) { 667 // Replay all graph attributes. 668 669 if (graph.getAttributeKeySet() != null) 670 for (String key : graph.getAttributeKeySet()) { 671 this.graph.addAttribute(key, graph.getAttribute(key)); 672 } 673 674 // Replay all nodes and their attributes. 675 676 for (Node node : graph) { 677 Node n = this.graph.addNode(node.getId()); 678 679 if (node.getAttributeKeySet() != null) { 680 for (String key : node.getAttributeKeySet()) { 681 n.addAttribute(key, node.getAttribute(key)); 682 } 683 } 684 } 685 686 // Replay all edges and their attributes. 687 688 for (Edge edge : graph.getEachEdge()) { 689 Edge e = this.graph.addEdge(edge.getId(), edge.getSourceNode() 690 .getId(), edge.getTargetNode().getId(), edge.isDirected()); 691 692 if (edge.getAttributeKeySet() != null) { 693 for (String key : edge.getAttributeKeySet()) { 694 e.addAttribute(key, edge.getAttribute(key)); 695 } 696 } 697 } 698 } 699}