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}