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.layout.springbox;
033
034import java.io.FileNotFoundException;
035import java.io.PrintStream;
036import java.util.HashMap;
037import java.util.Locale;
038import java.util.Random;
039
040import org.graphstream.stream.Sink;
041import org.graphstream.stream.SourceBase;
042import org.graphstream.stream.sync.SinkTime;
043import org.graphstream.ui.geom.Point3;
044import org.graphstream.ui.graphicGraph.GraphPosLengthUtils;
045import org.graphstream.ui.layout.Layout;
046import org.miv.pherd.ParticleBox;
047import org.miv.pherd.ParticleBoxListener;
048import org.miv.pherd.ntree.Anchor;
049import org.miv.pherd.ntree.CellSpace;
050import org.miv.pherd.ntree.OctreeCellSpace;
051import org.miv.pherd.ntree.QuadtreeCellSpace;
052
053/**
054 * Base implementation of a Barnes-Hut space decomposition and particle
055 * interaction algorithm to be used for force-based layout algorithms.
056 * 
057 * <p>
058 * This base class creates the space decomposition method and manages the
059 * main loop of the simulation. The simulation is made of {@link NodeParticle}
060 * and {@link EdgeSpring} elements that are created and linked for you in
061 * response to graph events received via the {@link Sink} interface. However
062 * you have to provide an implementation of the abstract {@link NodeParticle} class
063 * (by overriding the abstract method {@link #newNodeParticle(String)}). 
064 * </p>
065 * 
066 * <p>
067 * As almost all the repulsion/attraction forces computation is done in the
068 * {@link NodeParticle} class, this is the most important one.
069 * </p>
070 * 
071 * <p>
072 * This algorithm can be configured using several attributes put on the graph :
073 * <ul>
074 * <li>layout.force : a floating point number (default 0.5f), that allows to
075 * define the importance of movement of each node at each computation step. The
076 * larger the value the quicker nodes move to their position of lowest energy.
077 * However too high values can generate non stable layouts and oscillations.</li>
078 * <li>layout.quality : an integer between 0 and 4. With value 0 the layout is
079 * faster but it also can be farther from equilibrium. With value 4 the
080 * algorithm tries to be as close as possible from equilibrium (the n-tree and
081 * Barnes-Hut algorithms are disabled), but the computation can take a lot of
082 * time (the algorithm becomes O(n^2)). TODO change this into layout.barneshut
083 * or something similar.</li>
084 * </ul>
085 * You can also put the following attributes on nodes :
086 * <ul>
087 * <li>layout.weight : The force of repulsion of a node. The larger the value,
088 * the more the node repulses its neighbors.</li>
089 * </ul>
090 * And on edges :
091 * <ul>
092 * <li>layout.weight : the multiplier for the desired edge length. By default
093 * the algorithm tries to make each edge of length one. This is the position of
094 * lowest energy for a spring. This coefficient allows to modify this target
095 * spring length. Value larger than one will make the edge longer. Values
096 * between 0 and 1 will make the edge smaller.</li>
097 * <li>layout.stabilization-limit : the stabilization of a layout is a number
098 * between 0 and 1. 1 means fully stable, but this value is rare. Therefore one
099 * can consider the layout stable at a lower value. The default is 0.9. You can
100 * fix it with this attribute.</li>
101 * </ul>
102 * </p>
103 */
104public abstract class BarnesHutLayout extends SourceBase implements Layout,
105                ParticleBoxListener {
106
107        /**
108         * The nodes representation and the n-tree. The particle-box is an
109         * implementation of a recursive space decomposition method that is used
110         * here to break the O(n^2) complexity into a Barnes-Hut algorithm that is
111         * closer to O(n log n).
112         */
113        protected ParticleBox nodes;
114
115        /**
116         * The set of edges.
117         */
118        protected HashMap<String, EdgeSpring> edges = new HashMap<String, EdgeSpring>();
119
120        /**
121         * Used to avoid stabilizing if an event occurred.
122         */
123        protected int lastElementCount = 0;
124
125        /**
126         * Random number generator.
127         */
128        protected Random random;
129
130        /**
131         * The lowest node position.
132         */
133        protected Point3 lo = new Point3(0, 0, 0);
134
135        /**
136         * The highest node position.
137         */
138        protected Point3 hi = new Point3(1, 1, 1);
139        
140        /**
141         * The point in the middle of the layout.
142         */
143        protected Point3 center = new Point3(0.5, 0.5, 0.5);
144
145        /**
146         * Output stream for statistics if in debug mode.
147         */
148        protected PrintStream statsOut;
149
150        /**
151         * Energy, and the history of energies.
152         */
153        protected Energies energies = new Energies();
154
155        /**
156         * Global force strength. This is a factor in [0..1] that is used to scale
157         * all computed displacements.
158         */
159        protected double force = 1f;
160
161        /**
162         * The view distance at which the cells of the n-tree are explored
163         * exhaustively, after this the poles are used. This is a multiple of k.
164         */
165        protected double viewZone = 5f;
166
167        /**
168         * The Barnes/Hut theta threshold to know if we use a pole or not.
169         */
170        protected double theta = .7f;
171
172        /**
173         * The quality level.
174         */
175        protected double quality = 1;
176
177        /**
178         * Number of nodes per space-cell.
179         */
180        protected int nodesPerCell = 10;
181
182        /**
183         * The diagonal of the graph area at the current step.
184         */
185        protected double area = 1;
186
187        /**
188         * The stabilization limit of this algorithm.
189         */
190        protected double stabilizationLimit = 0.9;
191
192        // Attributes -- Statistics
193
194        /**
195         * Current step.
196         */
197        protected int time;
198
199        /**
200         * The duration of the last step in milliseconds.
201         */
202        protected long lastStepTime;
203
204        /**
205         * The maximum length of a node displacement at the current step.
206         */
207        protected double maxMoveLength;
208
209        /**
210         * Average move length.
211         */
212        protected double avgLength;
213
214        /**
215         * Number of nodes that moved during last step.
216         */
217        protected int nodeMoveCount;
218
219        // Attributes -- Settings
220
221        /**
222         * Compute the third coordinate ?.
223         */
224        protected boolean is3D = false;
225
226        /**
227         * The gravity factor. If set to 0 the gravity computation is disabled.
228         */
229        protected double gravity = 0;
230        
231        /**
232         * Send node informations?.
233         */
234        protected boolean sendNodeInfos = false;
235
236        /**
237         * If true a file is created to output the statistics of the spring box
238         * algorithm.
239         */
240        protected boolean outputStats = false;
241
242        /**
243         * If true a file is created for each node (!!!) and its movement statistics
244         * are logged.
245         */
246        protected boolean outputNodeStats = false;
247
248        /**
249         * If greater than one, move events are sent only every N steps.
250         */
251        protected int sendMoveEventsEvery = 1;
252
253        /**
254         * Sink time.
255         */
256        protected SinkTime sinkTime;
257
258        /**
259         * New 2D Barnes-Hut simulation.
260         */
261        public BarnesHutLayout() {
262                this(false);
263        }
264
265        /**
266         * New Barnes-Hut simulation.
267         * 
268         * @param is3D
269         *            If true the simulation dimensions count is 3 else 2.
270         */
271        public BarnesHutLayout(boolean is3D) {
272                this(is3D, new Random(System.currentTimeMillis()));
273        }
274
275        /**
276         * New Barnes-Hut simulation.
277         * 
278         * @param is3D
279         *            If true the simulation dimensions count is 3 else 2.
280         * @param randomNumberGenerator
281         *            The random number generator to use.
282         */
283        public BarnesHutLayout(boolean is3D, Random randomNumberGenerator) {
284                CellSpace space;
285
286                this.is3D = is3D;
287                this.random = randomNumberGenerator;
288
289                if (is3D) {
290                        space = new OctreeCellSpace(new Anchor(-1, -1, -1), new Anchor(1,
291                                        1, 1));
292                } else {
293                        space = new QuadtreeCellSpace(new Anchor(-1, -1, -0.01f),
294                                        new Anchor(1, 1, 0.01f));
295                }
296
297                this.nodes = new ParticleBox(nodesPerCell, space,
298                                new GraphCellData());
299
300                nodes.addParticleBoxListener(this);
301                setQuality(quality);
302
303                sinkTime = new SinkTime();
304                sourceTime.setSinkTime(sinkTime);
305        }
306
307        public Point3 getLowPoint() {
308                org.miv.pherd.geom.Point3 p = nodes.getNTree().getLowestPoint();
309                lo.set(p.x, p.y, p.z);
310                return lo;
311        }
312
313        public Point3 getHiPoint() {
314                org.miv.pherd.geom.Point3 p = nodes.getNTree().getHighestPoint();
315                hi.set(p.x, p.y, p.z);
316                return hi;
317        }
318
319        public double randomXInsideBounds() {
320                org.miv.pherd.geom.Point3 c = ((GraphCellData)nodes.getNTree().getRootCell().getData()).center;
321                return c.x + (random.nextDouble()*2-1);
322                //org.miv.pherd.geom.Point3 lo = nodes.getNTree().getLowestPoint();
323                //org.miv.pherd.geom.Point3 hi = nodes.getNTree().getHighestPoint();
324                //return lo.x + ((hi.x - lo.x)*random.nextDouble());
325        }
326
327        public double randomYInsideBounds() {
328                org.miv.pherd.geom.Point3 c = ((GraphCellData)nodes.getNTree().getRootCell().getData()).center;
329                return c.y + (random.nextDouble()*2-1);
330                //org.miv.pherd.geom.Point3 lo = nodes.getNTree().getLowestPoint();
331                //org.miv.pherd.geom.Point3 hi = nodes.getNTree().getHighestPoint();
332                //return lo.y + ((hi.y - lo.y)*random.nextDouble());
333        }
334
335        public double randomZInsideBounds() {
336                org.miv.pherd.geom.Point3 c = ((GraphCellData)nodes.getNTree().getRootCell().getData()).center;
337                return c.z + (random.nextDouble()*2-1);
338                //org.miv.pherd.geom.Point3 lo = nodes.getNTree().getLowestPoint();
339                //org.miv.pherd.geom.Point3 hi = nodes.getNTree().getHighestPoint();
340                //return lo.z + ((hi.z - lo.z)*random.nextDouble());
341        }
342        
343        public Point3 getCenterPoint() {
344                return center;
345        }
346        
347        /**
348         * A gravity factor that attracts all nodes to the center of the layout to avoid flying components. If
349         * set to zero, the gravity computation is disabled.
350         * @return The gravity factor, usually between 0 and 1.
351         */
352        public double getGravityFactor() {
353                return gravity;
354        }
355        
356        /**
357         * Set the gravity factor that attracts all nodes to the center of the layout to avoid flying components. If
358         * set to zero, the gravity computation is disabled.
359         * @param value The new gravity factor, usually between 0 and 1.
360         */
361        public void setGravityFactor(double value) {
362                gravity = value;
363        }
364
365        /**
366         * The spatial index as a n-tree.
367         * 
368         * @return The n-tree.
369         */
370        public ParticleBox getSpatialIndex() {
371                return nodes;
372        }
373
374        public long getLastStepTime() {
375                return lastStepTime;
376        }
377
378        public abstract String getLayoutAlgorithmName();
379
380        public int getNodeMovedCount() {
381                return nodeMoveCount;
382        }
383
384        public double getStabilization() {
385                if (lastElementCount == nodes.getParticleCount() + edges.size()) {
386                        if (time > energies.getBufferSize())
387                                return energies.getStabilization();
388                }
389
390                lastElementCount = nodes.getParticleCount() + edges.size();
391
392                return 0;
393        }
394
395        public double getStabilizationLimit() {
396                return stabilizationLimit;
397        }
398
399        public int getSteps() {
400                return time;
401        }
402
403        public double getQuality() {
404                return quality;
405        }
406
407        public boolean is3D() {
408                return is3D;
409        }
410
411        public double getForce() {
412                return force;
413        }
414
415        public Random getRandom() {
416                return random;
417        }
418
419        public Energies getEnergies() {
420                return energies;
421        }
422
423        /**
424         * The Barnes-Hut theta value used to know if we use a pole or not.
425         * 
426         * @return The theta value (between 0 and 1).
427         */
428        public double getBarnesHutTheta() {
429                return theta;
430        }
431
432        public double getViewZone() {
433                return viewZone;
434        }
435
436        public void setSendNodeInfos(boolean on) {
437                sendNodeInfos = on;
438        }
439
440        /**
441         * Change the barnes-hut theta parameter allowing to know if we use a pole
442         * or not.
443         * 
444         * @param theta
445         *            The new value for theta (between 0 and 1).
446         */
447        public void setBarnesHutTheta(double theta) {
448                if (theta > 0 && theta < 1) {
449                        this.theta = theta;
450                }
451        }
452
453        public void setForce(double value) {
454                this.force = value;
455        }
456
457        public void setStabilizationLimit(double value) {
458                this.stabilizationLimit = value;
459        }
460
461        public void setQuality(double qualityLevel) {
462                if (qualityLevel > 1)
463                        qualityLevel = 1;
464                else if (qualityLevel < 0)
465                        qualityLevel = 0;
466                quality = qualityLevel;
467        }
468
469        public void clear() {
470                energies.clearEnergies();
471                nodes.removeAllParticles();
472                edges.clear();
473                nodeMoveCount = 0;
474                lastStepTime = 0;
475        }
476
477        public void compute() {
478                long t1;
479
480                computeArea();
481
482                maxMoveLength = Double.MIN_VALUE;
483                t1 = System.currentTimeMillis();
484                nodeMoveCount = 0;
485                avgLength = 0;
486
487                // All the movement computation is done in this call.
488                nodes.step();
489
490                if (nodeMoveCount > 0)
491                        avgLength /= nodeMoveCount;
492
493                // Ready for the next step.
494
495                getLowPoint();
496                getHiPoint();
497                center.set(lo.x+(hi.x-lo.x)/2, lo.y+(hi.y-lo.y)/2, lo.z+(hi.z-lo.z)/2);
498                //center.set(0, 0, 0);
499                energies.storeEnergy();
500                printStats();
501                time++;
502                lastStepTime = System.currentTimeMillis() - t1;
503        }
504
505        /**
506         * Output some statistics on the layout process. This method is active only
507         * if {@link #outputStats} is true.
508         */
509        protected void printStats() {
510                if (outputStats) {
511                        if (statsOut == null) {
512                                try {
513                                        statsOut = new PrintStream("springBox.dat");
514                                        statsOut.printf("# stabilization nodeMoveCount energy energyDiff maxMoveLength avgLength area%n");
515                                        statsOut.flush();
516                                } catch (FileNotFoundException e) {
517                                        e.printStackTrace();
518                                }
519                        }
520
521                        if (statsOut != null) {
522                                double energyDiff = energies.getEnergy()
523                                                - energies.getPreviousEnergyValue(30);
524
525                                statsOut.printf(Locale.US, "%f %d %f %f %f %f%n",
526                                                getStabilization(), nodeMoveCount,
527                                                energies.getEnergy(), energyDiff, maxMoveLength,
528                                                avgLength, area);
529                                statsOut.flush();
530                        }
531                }
532        }
533
534        protected void computeArea() {
535                area = getHiPoint().distance(getLowPoint());
536        }
537
538        public void shake() {
539                energies.clearEnergies();
540        }
541
542        protected NodeParticle addNode(String sourceId, String id) {
543                NodeParticle np = newNodeParticle(id);
544                nodes.addParticle(np);
545                return np;
546        }
547
548        public void moveNode(String id, double x, double y, double z) {
549                NodeParticle node = (NodeParticle) nodes.getParticle(id);
550
551                if (node != null) {
552                        node.moveTo(x, y, z);
553                        energies.clearEnergies();
554                }
555        }
556
557        public void freezeNode(String id, boolean on) {
558                NodeParticle node = (NodeParticle) nodes.getParticle(id);
559
560                if (node != null) {
561                        node.frozen = on;
562                }
563        }
564
565        protected void setNodeWeight(String id, double weight) {
566                NodeParticle node = (NodeParticle) nodes.getParticle(id);
567
568                if (node != null)
569                        node.setWeight(weight);
570        }
571
572        protected void removeNode(String sourceId, String id) {
573                NodeParticle node = (NodeParticle) nodes.removeParticle(id);
574
575                if (node != null) {
576                        node.removeNeighborEdges();
577                } else {
578                        System.err.printf(
579                                        "layout %s: cannot remove non existing node %s%n",
580                                        getLayoutAlgorithmName(), id);
581                }
582        }
583
584        protected void addEdge(String sourceId, String id, String from, String to,
585                        boolean directed) {
586                NodeParticle n0 = (NodeParticle) nodes.getParticle(from);
587                NodeParticle n1 = (NodeParticle) nodes.getParticle(to);
588
589                if (n0 != null && n1 != null) {
590                        EdgeSpring e = new EdgeSpring(id, n0, n1);
591                        EdgeSpring o = edges.put(id, e);
592
593                        if (o != null) {
594                                // throw new SingletonException(
595                                // "edge '"+id+"' already exists");
596                                System.err.printf("layout %s: edge '%s' already exists%n",
597                                                getLayoutAlgorithmName(), id);
598                        } else {
599                                n0.registerEdge(e);
600                                n1.registerEdge(e);
601                        }
602
603                        chooseNodePosition(n0, n1);
604                } else {
605                        if (n0 == null)
606                                System.err
607                                                .printf("layout %s: node '%s' does not exist, cannot create edge %s%n",
608                                                                getLayoutAlgorithmName(), from, id);
609                        if (n1 == null)
610                                System.err
611                                                .printf("layout %s: node '%s' does not exist, cannot create edge %s%n",
612                                                                getLayoutAlgorithmName(), to, id);
613                }
614        }
615
616        /**
617         * Choose the best position for a node that was just connected by only one
618         * edge to a cluster of nodes.
619         * 
620         * @param n0
621         *            source node of the edge.
622         * @param n1
623         *            target node of the edge.
624         */
625        protected abstract void chooseNodePosition(NodeParticle n0, NodeParticle n1);
626
627        protected void addEdgeBreakPoint(String edgeId, int points) {
628                System.err.printf(
629                                "layout %s: edge break points are not handled yet.%n",
630                                getLayoutAlgorithmName());
631        }
632
633        protected void ignoreEdge(String edgeId, boolean on) {
634                EdgeSpring edge = edges.get(edgeId);
635
636                if (edge != null) {
637                        edge.ignored = on;
638                }
639        }
640
641        protected void setEdgeWeight(String id, double weight) {
642                EdgeSpring edge = edges.get(id);
643
644                if (edge != null)
645                        edge.weight = weight;
646        }
647
648        protected void removeEdge(String sourceId, String id) {
649                EdgeSpring e = edges.remove(id);
650
651                if (e != null) {
652                        e.node0.unregisterEdge(e);
653                        e.node1.unregisterEdge(e);
654                } else {
655                        System.err.printf(
656                                        "layout %s: cannot remove non existing edge %s%n",
657                                        getLayoutAlgorithmName(), id);
658                }
659        }
660
661        // Particle box listener
662
663        public void particleAdded(Object id, double x, double y, double z,
664                        Object mark) {
665        }
666
667        public void particleAdded(Object id, double x, double y, double z) {
668        }
669
670        public void particleMarked(Object id, Object mark) {
671        }
672
673        public void particleMoved(Object id, double x, double y, double z) {
674                if ((time % sendMoveEventsEvery) == 0) {
675                        Object xyz[] = new Object[3];
676                        xyz[0] = x;
677                        xyz[1] = y;
678                        xyz[2] = z;
679
680                        sendNodeAttributeChanged(sourceId, (String) id, "xyz", xyz, xyz);
681                }
682        }
683
684        public void particleRemoved(Object id) {
685        }
686
687        public void stepFinished(int time) {
688        }
689
690        public void particleAttributeChanged(Object id, String attribute,
691                        Object newValue, boolean removed) {
692        }
693
694        // SourceBase interface
695
696        public void edgeAdded(String graphId, long time, String edgeId,
697                        String fromNodeId, String toNodeId, boolean directed) {
698                if (sinkTime.isNewEvent(graphId, time)) {
699                        addEdge(graphId, edgeId, fromNodeId, toNodeId, directed);
700                        sendEdgeAdded(graphId, time, edgeId, fromNodeId, toNodeId, directed);
701                }
702        }
703
704        public void nodeAdded(String graphId, long time, String nodeId) {
705                if (sinkTime.isNewEvent(graphId, time)) {
706//System.err.printf("## before (%.2f, %.2f  --  %.2f %.2f) %n", lo.x, lo.y, hi.x, hi.y);
707                        NodeParticle np = addNode(graphId, nodeId);
708                        sendNodeAdded(graphId, time, nodeId);
709//                      org.miv.pherd.geom.Point3 p = np.getPosition();
710
711//System.err.printf("-- node added at (%.2f, %.2f)   inside (%.2f, %.2f  --  %.2f %.2f) %n", p.x, p.y, lo.x, lo.y, hi.x, hi.y);
712//                      particleMoved(nodeId, p.x, p.y, p.z);
713                }
714        }
715
716        public void edgeRemoved(String graphId, long time, String edgeId) {
717                if (sinkTime.isNewEvent(graphId, time)) {
718                        removeEdge(graphId, edgeId);
719                        sendEdgeRemoved(graphId, time, edgeId);
720                }
721        }
722
723        public void nodeRemoved(String graphId, long time, String nodeId) {
724                if (sinkTime.isNewEvent(graphId, time)) {
725                        removeNode(graphId, nodeId);
726                        sendNodeRemoved(graphId, time, nodeId);
727                }
728        }
729
730        public void graphCleared(String graphId, long time) {
731                if (sinkTime.isNewEvent(graphId, time)) {
732                        clear();
733                        sendGraphCleared(graphId, time);
734                }
735        }
736
737        public void stepBegins(String graphId, long time, double step) {
738                if (sinkTime.isNewEvent(graphId, time)) {
739                        sendStepBegins(graphId, time, step);
740                }
741        }
742
743        public void graphAttributeAdded(String graphId, long time,
744                        String attribute, Object value) {
745                if (sinkTime.isNewEvent(graphId, time)) {
746                        graphAttributeChanged_(graphId, attribute, null, value);
747                        sendGraphAttributeAdded(graphId, time, attribute, value);
748                }
749        }
750
751        public void graphAttributeChanged(String graphId, long time,
752                        String attribute, Object oldValue, Object newValue) {
753                if (sinkTime.isNewEvent(graphId, time)) {
754                        graphAttributeChanged_(graphId, attribute, oldValue, newValue);
755                        sendGraphAttributeChanged(graphId, time, attribute, oldValue,
756                                        newValue);
757                }
758        }
759
760        protected void graphAttributeChanged_(String graphId, String attribute,
761                        Object oldValue, Object newValue) {
762                if (attribute.equals("layout.force")) {
763                        if (newValue instanceof Number)
764                                setForce(((Number) newValue).doubleValue());
765                        // System.err.printf("layout.elasticBox.force: %f%n",
766                        // ((Number) newValue).doubleValue());
767
768                        energies.clearEnergies();
769                } else if (attribute.equals("layout.quality")) {
770                        if (newValue instanceof Number) {
771                                int q = ((Number) newValue).intValue();
772
773                                q = q > 4 ? 4 : q;
774                                q = q < 0 ? 0 : q;
775
776                                setQuality(q);
777                                System.err.printf("layout.%s.quality: %d%n", getLayoutAlgorithmName(), q);
778                        }
779
780                        energies.clearEnergies();
781                } else if(attribute.equals("layout.gravity")) {
782                        if(newValue instanceof Number) {
783                                double value = ((Number)newValue).doubleValue();
784                                setGravityFactor(value);
785                                System.err.printf("layout.%s.gravity: %f%n", getLayoutAlgorithmName(), value);
786                        }
787                } else if (attribute.equals("layout.exact-zone")) {
788                        if (newValue instanceof Number) {
789                                double factor = ((Number) newValue).doubleValue();
790
791                                factor = factor > 1 ? 1 : factor;
792                                factor = factor < 0 ? 0 : factor;
793
794                                viewZone = factor;
795                                System.err.printf(
796                                                "layout.%s.exact-zone: %f of [0..1]%n", getLayoutAlgorithmName(),
797                                                viewZone);
798
799                                energies.clearEnergies();
800                        }
801                } else if (attribute.equals("layout.output-stats")) {
802                        if (newValue == null)
803                                outputStats = false;
804                        else
805                                outputStats = true;
806
807                        System.err.printf("layout.%s.output-stats: %b%n", getLayoutAlgorithmName(),
808                                        outputStats);
809                } else if (attribute.equals("layout.stabilization-limit")) {
810                        if (newValue instanceof Number) {
811                                stabilizationLimit = ((Number) newValue).doubleValue();
812                                if (stabilizationLimit > 1)
813                                        stabilizationLimit = 1;
814                                else if (stabilizationLimit < 0)
815                                        stabilizationLimit = 0;
816
817                                energies.clearEnergies();
818                        }
819                }
820        }
821
822        public void graphAttributeRemoved(String graphId, long time,
823                        String attribute) {
824                if (sinkTime.isNewEvent(graphId, time)) {
825                        sendGraphAttributeRemoved(graphId, time, attribute);
826                }
827        }
828
829        public void nodeAttributeAdded(String graphId, long time, String nodeId,
830                        String attribute, Object value) {
831                if (sinkTime.isNewEvent(graphId, time)) {
832                        nodeAttributeChanged_(graphId, nodeId, attribute, null, value);
833                        sendNodeAttributeAdded(graphId, time, nodeId, attribute, value);
834                }
835        }
836
837        public void nodeAttributeChanged(String graphId, long time, String nodeId,
838                        String attribute, Object oldValue, Object newValue) {
839                if (sinkTime.isNewEvent(graphId, time)) {
840                        nodeAttributeChanged_(graphId, nodeId, attribute, oldValue,
841                                        newValue);
842                        sendNodeAttributeChanged(graphId, time, nodeId, attribute,
843                                        oldValue, newValue);
844                }
845        }
846
847        protected void nodeAttributeChanged_(String graphId, String nodeId,
848                        String attribute, Object oldValue, Object newValue) {
849                if (attribute.equals("layout.weight")) {
850                        if (newValue instanceof Number)
851                                setNodeWeight(nodeId, ((Number) newValue).doubleValue());
852                        else if (newValue == null)
853                                setNodeWeight(nodeId, 1);
854
855                        energies.clearEnergies();
856                } else if (attribute.equals("layout.frozen")) {
857                        freezeNode(nodeId, (newValue != null));
858//System.err.printf("user froze node %s (%b)%n", nodeId, (newValue != null));
859                } else if (attribute.equals("xyz") || attribute.equals("xy")) {
860                        double xyz[] = new double[3];
861                        GraphPosLengthUtils.positionFromObject(newValue, xyz);
862                        moveNode(nodeId, xyz[0], xyz[1], xyz[2]);
863//System.err.printf("user moved node %s to %f %f%n", nodeId, xyz[0], xyz[1]);
864                } else if (attribute.equals("x") && newValue instanceof Number) {
865                        NodeParticle node = (NodeParticle) nodes.getParticle(nodeId);
866                        if (node != null) {
867                                moveNode(nodeId, ((Number) newValue).doubleValue(),
868                                                node.getPosition().y, node.getPosition().z);
869                        }
870                } else if (attribute.equals("y") && newValue instanceof Number) {
871                        NodeParticle node = (NodeParticle) nodes.getParticle(nodeId);
872                        if (node != null) {
873                                moveNode(nodeId, node.getPosition().x,
874                                                ((Number) newValue).doubleValue(), node.getPosition().z);
875                        }
876                }
877        }
878
879        public void nodeAttributeRemoved(String graphId, long time, String nodeId,
880                        String attribute) {
881                if (sinkTime.isNewEvent(graphId, time)) {
882                        nodeAttributeChanged_(graphId, nodeId, attribute, null, null);
883                        sendNodeAttributeRemoved(graphId, time, nodeId, attribute);
884                }
885        }
886
887        public void edgeAttributeAdded(String graphId, long time, String edgeId,
888                        String attribute, Object value) {
889                if (sinkTime.isNewEvent(graphId, time)) {
890                        edgeAttributeChanged_(graphId, edgeId, attribute, null, value);
891                        sendEdgeAttributeAdded(graphId, time, edgeId, attribute, value);
892                }
893        }
894
895        public void edgeAttributeChanged(String graphId, long time, String edgeId,
896                        String attribute, Object oldValue, Object newValue) {
897                if (sinkTime.isNewEvent(graphId, time)) {
898                        edgeAttributeChanged_(graphId, edgeId, attribute, oldValue,
899                                        newValue);
900                        sendEdgeAttributeChanged(graphId, time, edgeId, attribute,
901                                        oldValue, newValue);
902                }
903        }
904
905        protected void edgeAttributeChanged_(String graphId, String edgeId,
906                        String attribute, Object oldValue, Object newValue) {
907                if (attribute.equals("layout.weight")) {
908                        if (newValue instanceof Number)
909                                setEdgeWeight(edgeId, ((Number) newValue).doubleValue());
910                        else if (newValue == null)
911                                setEdgeWeight(edgeId, 1);
912
913                        energies.clearEnergies();
914                } else if (attribute.equals("layout.ignored")) {
915                        if (newValue instanceof Boolean)
916                                ignoreEdge(edgeId, (Boolean) newValue);
917                        energies.clearEnergies();
918                }
919        }
920
921        public void edgeAttributeRemoved(String graphId, long time, String edgeId,
922                        String attribute) {
923                if (sinkTime.isNewEvent(graphId, time)) {
924                        edgeAttributeChanged_(graphId, edgeId, attribute, null, null);
925                        sendEdgeAttributeRemoved(attribute, time, edgeId, attribute);
926                }
927        }
928
929        /**
930         * Factory method to create node particles.
931         * 
932         * @param id
933         *            The identifier of the new node/particle.
934         * @return The new node/particle.
935         */
936        public abstract NodeParticle newNodeParticle(String id);
937}