1 /*
2  *  SPDX-FileCopyrightText: 2011-2014 Andreas Cord-Landwehr <cordlandwehr@kde.org>
3  *
4  *  SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5  */
6 
7 #include "topology.h"
8 #include "graphdocument.h"
9 #include "edge.h"
10 #include "logging_p.h"
11 
12 #include <algorithm>
13 
14 #include <QList>
15 #include <QPair>
16 #include <QVector>
17 #include <QVector2D>
18 #include <QStack>
19 #include <QQueue>
20 #include <QtMath>
21 #include <QPointF>
22 #include <QRandomGenerator>
23 
24 #include <boost/graph/fruchterman_reingold.hpp>
25 #include <boost/graph/circle_layout.hpp>
26 #include <boost/graph/random_layout.hpp>
27 #include <boost/graph/adjacency_list.hpp>
28 #include <boost/graph/topology.hpp>
29 #include <boost/lexical_cast.hpp>
30 #include <boost/random/linear_congruential.hpp>
31 
32 using namespace GraphTheory;
33 
34 typedef boost::adjacency_list < boost::listS, boost::vecS, boost::undirectedS,
35         boost::property<boost::vertex_name_t, std::string> >
36         Graph;
37 typedef boost::rectangle_topology<> topology_type;
38 typedef topology_type::point_type point_type;
39 typedef QVector<point_type> PositionVec;
40 typedef boost::iterator_property_map < PositionVec::iterator,
41         boost::property_map<Graph, boost::vertex_index_t>::type >
42         PositionMap;
43 typedef boost::graph_traits<Graph>::vertex_descriptor Vertex;
44 typedef QPair<int, int> BoostEdge;
45 
46 
47 typedef QPair<int, int> RemappedEdge;
48 struct RemappedGraph {
49     int numberOfNodes;
50     QMap<NodePtr, int> nodeToIndexMap;
51     QVector<RemappedEdge> edges;
52     QVector<QVector<int>> adjacency;
53 };
54 
55 // handle boost exceptions
56 namespace boost {
throw_exception(std::exception const & e)57     void throw_exception(std::exception const &e) {
58         qCCritical(GRAPHTHEORY_GENERAL) << "Exception:" << e.what();
59     }
60 }
61 
applyMinCutTreeAlignment(const NodeList & nodes)62 void Topology::applyMinCutTreeAlignment(const NodeList &nodes)
63 {
64     // nodes must be at least of length 2, and two nodes cannot have crossing edges
65     if (nodes.count() < 3) {
66         return;
67     }
68 
69     PositionVec position_vec(nodes.count());
70 
71     // set box inside which we may reposition
72     QList<qreal> xList;
73     QList<qreal> yList;
74     for (const NodePtr& node : nodes) {
75         xList << node->x();
76         yList << node->y();
77     }
78     std::sort(xList.begin(), xList.end());
79     std::sort(yList.begin(), yList.end());
80 
81     // do not perform algorithm if graph is very dense:
82     // this prevents very long algorithm computations and possible threading issues
83     if (xList.last() - xList.first() < 10 && yList.last() - yList.first() < 10 ) {
84         qCDebug(GRAPHTHEORY_GENERAL) << "Aborting min cut alignment: nodes are already close to each other.";
85         return;
86     }
87 
88     topology_type topology(xList.first(), yList.first(), xList.last(), yList.last());
89 
90     // create IDs for all nodes
91     QMap<NodePtr, int> node_mapping;
92     QMap<QPair<int, int>, EdgePtr > edge_mapping; // to map all edges back afterwards
93     int counter = 0;
94     for (const NodePtr& node :nodes) {
95         node_mapping[node] = counter++;
96     }
97 
98     const auto documentEdges = nodes.first()->document()->edges();
99     QVector<BoostEdge> edges(documentEdges.size());
100 
101     counter = 0;
102     for (const EdgePtr& edge : documentEdges) {
103         edges[counter++] = BoostEdge(node_mapping[edge->from()], node_mapping[edge->to()]);
104     }
105 
106     // setup the graph
107     Graph graph(
108         edges.begin(),
109         edges.end(),
110         nodes.count()
111     );
112 
113     PositionMap positionMap(position_vec.begin(), get(boost::vertex_index, graph));
114     counter = 0;
115     for (const NodePtr &node : nodes) {
116         positionMap[counter][0] = node->x();
117         positionMap[counter][1] = node->y();
118         counter++;
119     }
120 
121     // minimize cuts by Fruchtman-Reingold layout algorithm
122     boost::fruchterman_reingold_force_directed_layout< topology_type, Graph, PositionMap >
123     (graph,
124      positionMap,
125      topology,
126      boost::cooling(boost::linear_cooling<double>(100))
127     );
128 
129     // put nodes at whiteboard as generated
130     for (const NodePtr& node : nodes) {
131         Vertex v = boost::vertex(node_mapping[node], graph);
132         node->setX(positionMap[v][0]);
133         node->setY(positionMap[v][1]);
134     }
135 }
136 
applyCircleAlignment(const NodeList & nodes,qreal radius)137 void Topology::applyCircleAlignment(const NodeList &nodes, qreal radius)
138 {
139     if (nodes.length() == 0) {
140         return;
141     }
142 
143     PositionVec position_vec(nodes.count());
144 
145     if(radius == 0) {
146         // set box inside which we may reposition
147         QList<qreal> xList;
148         QList<qreal> yList;
149         for (const NodePtr& node : nodes) {
150             xList << node->x();
151             yList << node->y();
152         }
153         std::sort(xList.begin(), xList.end());
154         std::sort(yList.begin(), yList.end());
155 
156         radius = fmax(fabs(xList.first() - xList.last()), fabs(yList.first() - yList.last())) / 2;
157     }
158 
159     // create IDs for all nodes
160     QMap<NodePtr, int> node_mapping;
161     QMap<QPair<int, int>, EdgePtr > edge_mapping; // to map all edges back afterwards
162     int counter = 0;
163     for (const NodePtr &node : nodes) {
164         node_mapping[node] = counter++;
165     }
166 
167     const auto documentEdges = nodes.first()->document()->edges();
168     QVector<BoostEdge> edges(documentEdges.size());
169 
170     counter = 0;
171     for (const EdgePtr& edge : documentEdges) {
172         edges[counter++] = BoostEdge(node_mapping[edge->from()], node_mapping[edge->to()]);
173     }
174 
175     // setup the graph
176     Graph graph(
177         edges.begin(),
178         edges.end(),
179         nodes.count()
180     );
181 
182     PositionMap positionMap(position_vec.begin(), get(boost::vertex_index, graph));
183     counter = 0;
184     for (const NodePtr &node : nodes) {
185         positionMap[counter][0] = node->x();
186         positionMap[counter][1] = node->y();
187         counter++;
188     }
189 
190     // layout to circle
191     boost::circle_graph_layout<Graph, PositionMap>( graph,
192                                                     positionMap,
193                                                     radius);
194 
195     // put nodes at whiteboard as generated
196     for (const NodePtr &node : nodes) {
197         Vertex v = boost::vertex(node_mapping[node], graph);
198         node->setX(positionMap[v][0]);
199         node->setY(positionMap[v][1]);
200     }
201 }
202 
203 
directedGraphDefaultTopology(GraphDocumentPtr document)204 void Topology::directedGraphDefaultTopology(GraphDocumentPtr document)
205 {
206     //TODO: port to graphviz layout functions
207     qCDebug(GRAPHTHEORY_GENERAL) << "Temporary implementation, should be replaced soon.";
208     applyCircleAlignment(document->nodes(), 300);
209     applyMinCutTreeAlignment(document->nodes());
210 }
211 
212 
undirectedGraphDefaultTopology(GraphDocumentPtr document)213 void Topology::undirectedGraphDefaultTopology(GraphDocumentPtr document)
214 {
215     //TODO: port to graphviz layout functions
216     qCDebug(GRAPHTHEORY_GENERAL) << "Temporary implementation, should be replaced soon.";
217     applyCircleAlignment(document->nodes(), 300);
218     applyMinCutTreeAlignment(document->nodes());
219 }
220 
221 /** @brief Calculates the size of a square in which a number of circles fit well.
222  *
223  * Given the number of circles and their radius, heuristically computes the length of the side
224  * of a square in which all circles can easily be placed without intersecting each other.
225  * Use this to figure out how big a square should be contain the drawing of graph well.
226  *
227  * By easily, understand the following:
228  * Consider a square with side length computed by this method. An algorithm that places circles
229  * at random positions inside this square, with uniform probability, is expected to generate
230  * little or no intersection between circles.
231  *
232  */
squareSideRandomPlacementHeuristic(const qreal radius,const int numberOfCircles)233 qreal squareSideRandomPlacementHeuristic(const qreal radius, const int numberOfCircles)
234 {
235     if (numberOfCircles < 2) {
236         return 2 * radius;
237     }
238 
239     //This formula was obtained by a mix of experimentation and calculations.
240     //return qMax(4.26 * numberOfCircles - 3.5, 2.) * radius;
241 
242     return qSqrt(15.59 * numberOfCircles - 21.32) * radius;
243 }
244 
245 /* Maps the nodes in a node list to indexes, starting from the index 0.
246  */
mapNodesToIndexes(const NodeList & nodes)247 QMap<NodePtr, int> mapNodesToIndexes(const NodeList& nodes)
248 {
249     int nextIndex = 0;
250     QMap<NodePtr, int> nodeToIndexMap;
251     for (const NodePtr& node : nodes) {
252         nodeToIndexMap[node] = nextIndex;
253         nextIndex++;
254     }
255     return nodeToIndexMap;
256 }
257 
getRemappedEdges(const EdgeList & edges,const QMap<NodePtr,int> & nodeToIndexMap)258 QVector<RemappedEdge> getRemappedEdges(const EdgeList& edges,
259                                        const QMap<NodePtr, int>& nodeToIndexMap)
260 {
261     QVector<RemappedEdge> remappedEdges;
262     for (const EdgePtr& edge : edges) {
263         const int from = nodeToIndexMap[edge->from()];
264         const int to = nodeToIndexMap[edge->to()];
265         remappedEdges.push_back(RemappedEdge(from, to));
266     }
267     return remappedEdges;
268 }
269 
270 /*
271  * Extracts the graph from a GraphDocument to a representation that is more convenient for
272  * the graph layout algorithms.
273  */
remapGraph(const GraphDocumentPtr document)274 RemappedGraph remapGraph(const GraphDocumentPtr document) {
275     RemappedGraph remappedGraph;
276     remappedGraph.numberOfNodes = document->nodes().size();
277     remappedGraph.nodeToIndexMap = mapNodesToIndexes(document->nodes());
278     remappedGraph.edges = getRemappedEdges(document->edges(), remappedGraph.nodeToIndexMap);
279     remappedGraph.adjacency.resize(remappedGraph.numberOfNodes);
280     for (const QPair<int, int>& edge : remappedGraph.edges) {
281         remappedGraph.adjacency[edge.first].push_back(edge.second);
282         remappedGraph.adjacency[edge.second].push_back(edge.first);
283     }
284     return remappedGraph;
285 }
286 
287 /* Updates the positions of the nodes in a NodeList.
288  *
289  * For performance reasons, this should not be done all the time.
290  * Every time a coordinate of a node changes, a Qt signal is emitted.
291  */
moveNodes(const NodeList & nodes,const QMap<NodePtr,int> & nodeToIndexMap,const QVector<QPointF> & positions)292 void moveNodes(const NodeList& nodes, const QMap<NodePtr, int>& nodeToIndexMap,
293                const QVector<QPointF>& positions)
294 {
295     for (const NodePtr& node : nodes) {
296         const int index = nodeToIndexMap[node];
297         const QPointF& position = positions[index];
298         node->setX(position.x());
299         node->setY(position.y());
300     }
301 }
302 
303 /* Extracts the current positions of the nodes in a NodeList.
304  */
getCurrentPositions(const NodeList & nodes,const QMap<NodePtr,int> & nodeToIndexMap)305 QVector<QPointF> getCurrentPositions(const NodeList& nodes,
306                                     const QMap<NodePtr, int>& nodeToIndexMap)
307 {
308     QVector<QPointF> positions(nodes.size());
309     for (const NodePtr& node : nodes) {
310         const int index = nodeToIndexMap[node];
311         positions[index] = QPointF(node->x(), node->y());
312     }
313     return positions;
314 }
315 
316 
317 /* Computes a unit vector with direction chosen at random.
318  * All the directions have the same probability of being chosen.
319  */
randomDirection(QRandomGenerator & randomGenerator)320 QVector2D randomDirection(QRandomGenerator& randomGenerator) {
321     const qreal angle = randomGenerator.bounded(2 * M_PI);
322     return QVector2D(qCos(angle), qSin(angle));
323 }
324 
325 /* Given point, computes the closest point to it that lies inside the specified rectangle.
326  */
projectIntoRectangle(const QPointF & point,const qreal minX,const qreal maxX,const qreal & minY,const qreal & maxY)327 QPointF projectIntoRectangle(const QPointF& point, const qreal minX, const qreal maxX,
328                                const qreal& minY, const qreal& maxY)
329 {
330     const qreal x = qMin(qMax(point.x(), minX), maxX);
331     const qreal y = qMin(qMax(point.y(), minY), maxY);
332     return QPointF(x, y);
333 }
334 
335 /**
336  * Lays a graph out using an adaptation of the Fruchterman-Reingold algorithm.
337  *
338  * @param graph The graph to be laid out.
339  * @param areaFactor A positive constant that imprecisely indicates how spread the graph should be.
340  * @param repellingForce A constant that controls how strong is the repelling force between nodes.
341  * @param attractionForce A constant that controls how strong is the attraction force between nodes
342  *                        that are neighbours.
343  * @param minX is the minimum x-coordinate the center of a node can have.
344  * @param maxX is the maximum x-coordinate the center of a node can have.
345  * @param minY is the minimum y-coordinate the center of a node can have.
346  * @param maxY is the maximum y-coordinate the center of a node can have.
347  * @param nodeRadius is the radius of each node.
348  * @param initialTemperature is the temperature to start the simulation.
349  * @param numberOfIterations Number of iterations to be realized in the simulation.
350  * @param initialPositions The initial positions of the nodes.
351  * @param randomGenerator The random number generator engine to be used if necessary.
352  *
353  * @return The position of the nodes after the simulation.
354  */
forceBasedLayout(const RemappedGraph & graph,const qreal areaFactor,const qreal repellingForce,const qreal attractionForce,const qreal minX,const qreal maxX,const qreal minY,const qreal maxY,const qreal nodeRadius,const qreal initialTemperature,const int numberOfIterations,const QVector<QPointF> & initialPositions,QRandomGenerator & randomGenerator)355 QVector<QPointF> forceBasedLayout(const RemappedGraph& graph, const qreal areaFactor,
356                                   const qreal repellingForce, const qreal attractionForce,
357                                   const qreal minX, const qreal maxX, const qreal minY,
358                                   const qreal maxY, const qreal nodeRadius,
359                                   const qreal initialTemperature, const int numberOfIterations,
360                                   const QVector<QPointF>& initialPositions,
361                                   QRandomGenerator& randomGenerator)
362 {
363     Q_ASSERT(maxX > minX);
364     Q_ASSERT(maxY > minY);
365     Q_ASSERT(areaFactor > 0.);
366     Q_ASSERT(graph.numberOfNodes > 0);
367 
368     //Constant used to calculate the forces acting on each node.
369     const qreal area = (maxX - minX) * (maxY - minY);
370     const qreal k = areaFactor * qSqrt(area / graph.numberOfNodes);
371 
372     //Length of the diagonal of the rectangle.
373     const qreal diagonalLength = qSqrt(qPow(maxX - minX, 2) + qPow(maxY - minY, 2));
374 
375     //Maximum distance at which repelling forces do act.
376     const qreal repellingForceRadius = qMax(3 * nodeRadius, diagonalLength / 2);
377 
378     QVector<QPointF> currentPositions = initialPositions;
379     QVector<QVector2D> nodeForce(graph.numberOfNodes);
380     for (int iteration = 0; iteration < numberOfIterations; iteration++) {
381         //Clear forces from the previous iteration.
382         nodeForce.fill(QVector2D());
383 
384         //Calculates the repelling forces.
385         for (int i = 0; i < graph.numberOfNodes; i++) {
386             for (int j = i + 1; j < graph.numberOfNodes; j++) {
387                 QVector2D direction(currentPositions[j] - currentPositions[i]);
388                 const qreal distance = direction.length();
389 
390                 //Avoid using repelling forces between nodes that are too far from each other.
391                 //Even when small, this forces tend to make nodes go to the sides of the rectangle.
392                 if (distance > repellingForceRadius) {
393                     continue;
394                 }
395 
396                 //Adaptation of the original Fruchterman-Reingold calculation to consider the
397                 //radius of nodes, avoiding intersection between pairs of nodes.
398                 //Using k insted of k * k in the force calculation seems to lead to better results.
399                 const qreal correctedDistance = qMax(distance - 2 * nodeRadius,
400                                                      1. / graph.numberOfNodes);
401                 const qreal force = repellingForce * k * k / correctedDistance;
402 
403                 //If the distance is too small, pick a random direction to avoid the case in
404                 //which two nodes have the same position.
405                 if (distance < nodeRadius) {
406                     direction = randomDirection(randomGenerator);
407                 } else {
408                     direction.normalize();
409                 }
410 
411                 nodeForce[i] -= force * direction;
412                 nodeForce[j] += force * direction;
413             }
414         }
415 
416         //Calculates the attraction forces.
417         for (const RemappedEdge& edge : graph.edges) {
418             const int i = edge.first;
419             const int j = edge.second;
420             QVector2D direction(currentPositions[j] - currentPositions[i]);
421             const qreal distance = direction.length();
422 
423             //Do not use attraction forces between nodes that are already too close.
424             if (distance < 3 * nodeRadius) {
425                 continue;
426             }
427 
428             direction.normalize();
429             const qreal force = attractionForce * distance * distance / k;
430 
431             nodeForce[i] += force * direction;
432             nodeForce[j] -= force * direction;
433         }
434 
435 
436         //Calculates the current temperature using a liner cooling schedule.
437         const qreal temperature = initialTemperature * (numberOfIterations - iteration) /
438                                   numberOfIterations;
439 
440         //Moves nodes, keeping their coordinates inside the allowed ranges.
441         for (int i = 0; i < graph.numberOfNodes; i++) {
442             const qreal displacement = qMin<qreal>(nodeForce[i].length(), temperature);
443             const QVector2D direction = nodeForce[i].normalized();
444             const QPointF target(currentPositions[i].x() + displacement * direction.x(),
445                                  currentPositions[i].y() + displacement * direction.y());
446             currentPositions[i] = projectIntoRectangle(target, minX, maxX, minY, maxY);
447         }
448 
449     }
450 
451     return currentPositions;
452 }
453 
454 /* Randomly assigns positions to the nodes of a graph. The positions of the nodes are
455  * chosen independently from each other with uniform probability inside the specified
456  * rectangle.
457  */
randomLayout(const RemappedGraph & graph,const qreal minX,const qreal maxX,const qreal minY,const qreal maxY,QRandomGenerator & randomGenerator)458 QVector<QPointF> randomLayout(const RemappedGraph& graph, const qreal minX, const qreal maxX,
459                               const qreal minY, const qreal maxY, QRandomGenerator& randomGenerator)
460 {
461     Q_ASSERT(maxX > minX);
462     Q_ASSERT(maxY > minY);
463     QVector<QPointF> positions(graph.numberOfNodes);
464     for (int i = 0; i < graph.numberOfNodes; i++) {
465         positions[i].setX(randomGenerator.bounded(maxX - minX) + minX);
466         positions[i].setY(randomGenerator.bounded(maxY - minY) + minY);
467     }
468     return positions;
469 }
470 
471 /* Translates de nodes of a graph so that the graph touches the left and the upper sides of the
472  * specified rectangle.
473  */
translateGraphToUpperLeftCorner(const qreal minX,const qreal maxX,const qreal minY,const qreal maxY,QVector<QPointF> & positions)474 void translateGraphToUpperLeftCorner(const qreal minX, const qreal maxX, const qreal minY,
475                                     const qreal maxY, QVector<QPointF>& positions)
476 {
477     qreal xDisplacement = maxX - minX;
478     qreal yDisplacement = maxY - minY;
479     for (const QPointF& point : positions) {
480         xDisplacement = qMin(xDisplacement, point.x() - minX);
481         yDisplacement = qMin(yDisplacement, point.y() - minY);
482     }
483 
484     for (QPointF& point : positions) {
485         point.setX(point.x() - xDisplacement);
486         point.setY(point.y() - yDisplacement);
487     }
488 }
489 
applyForceBasedLayout(GraphDocumentPtr document,const qreal nodeRadius,const qreal margin,const qreal areaFactor,const qreal repellingForce,const qreal attractionForce,const bool randomizeInitialPositions,const quint32 seed)490 void Topology::applyForceBasedLayout(GraphDocumentPtr document, const qreal nodeRadius,
491                                      const qreal margin, const qreal areaFactor,
492                                      const qreal repellingForce, const qreal attractionForce,
493                                      const bool randomizeInitialPositions, const quint32 seed)
494 {
495     //There is nothing to do with an empty graph.
496     if (document->nodes().empty()) {
497         return;
498     }
499 
500     QRandomGenerator randomGenerator(seed);
501 
502     //Gets a new representation of the graph for efficiency purposes
503     RemappedGraph graph = remapGraph(document);
504 
505     //Computes the square in which the center of the nodes should be placed.
506     //This is done heuristically so that there is enough room to move nodes around easily.
507     //Because the heuristic used considers only circles, one extra circle is created for each edge.
508     //The reasoning is that graphs with more edges need more space to drawn nicely.
509     const int numberOfCircles = graph.numberOfNodes + qMin(graph.edges.size(),
510                                                            5 * graph.numberOfNodes);
511     const qreal circleRadius = 2 * nodeRadius;
512     const qreal side = squareSideRandomPlacementHeuristic(circleRadius, numberOfCircles);
513     const qreal minX = margin + nodeRadius;
514     const qreal maxX = minX + side;
515     const qreal minY = margin + nodeRadius;
516     const qreal maxY = minY + side;
517 
518     //Computes the initial positions.
519     QVector<QPointF> initialPositions;
520     if (randomizeInitialPositions) {
521         initialPositions = randomLayout(graph, minX, maxX, minY,maxY, randomGenerator);
522     } else {
523         initialPositions = getCurrentPositions(document->nodes(), graph.nodeToIndexMap);
524     }
525 
526 
527     //In order to converge properly, it makes sense that the number of iterations increases as
528     //the number of nodes increases. For very small graphs, a minimum number of iterations
529     //should be realized so the algorithm has the chance to find a nice layout.
530     const int numberOfIterations = qMax(graph.numberOfNodes, 100);
531 
532     //The temperature indicates how far a node can be moved in a single iteration.
533     //Initially, this value should be big enough to enable every node to go anywhere.
534     //Here, this is set so that after freeIterations each node can be anywhere.
535     const int freeIterations = numberOfIterations / 10 + 1;
536     const qreal initialTemperature = 2. * qSqrt(2.) * side * numberOfIterations / freeIterations /
537                                      (2 * numberOfIterations - freeIterations + 1);
538 
539     //Computes the layout.
540     QVector<QPointF> positions = forceBasedLayout(graph, areaFactor, repellingForce,
541                                                   attractionForce, minX, maxX, minY, maxY,
542                                                   nodeRadius, initialTemperature,
543                                                   numberOfIterations, initialPositions,
544                                                   randomGenerator);
545 
546 
547     //The generated layout may have some unused space above the graph and to the left of the graph.
548     translateGraphToUpperLeftCorner(minX, maxX, minY, maxY, positions);
549 
550 
551     //Moves nodes to their final positions.
552     moveNodes(document->nodes(), graph.nodeToIndexMap, positions);
553 }
554 
555 /* Returns the children of @p node in a Depth-First-Search tree.
556  *
557  * @param graph The graph being searched.
558  * @param visited Indicates which nodes have already been visited by the Depth-First-Search.
559  * @param node The current node of the search.
560  */
getChildren(const RemappedGraph & graph,const QVector<bool> & visited,const int node)561 QVector<int> getChildren(const RemappedGraph& graph, const QVector<bool>& visited, const int node)
562 {
563     QVector<int> children;
564     for (const int neighbour : graph.adjacency[node]) {
565         if (not visited[neighbour]) {
566             children.push_back(neighbour);
567         }
568     }
569     return children;
570 }
571 
572 /* Reorders the children heuristically to avoid the need of very long edges.
573  *
574  * The length of the edges between a node and its children depends on the angle formed by the
575  * centers of the two children and the origin. If this angle is very small, long edges are needed
576  * to avoid the intersection of two nodes. The smallest of these angles is determined by the
577  * number of leafs in the sub-trees rooted at two consecutive nodes. This function mixes
578  * nodes with small and large number of leafs in their sub-trees in order to avoid having two
579  * consecutive children with a small number of leafs in their sub-trees.
580  */
reorderChildrenForRadialLayout(const QVector<int> & numberOfLeafs,QVector<int> & children)581 void reorderChildrenForRadialLayout(const QVector<int>& numberOfLeafs, QVector<int>& children)
582 {
583     const auto numberOfLeafsComparator = [&numberOfLeafs](const int i, const int j) {
584             return numberOfLeafs[i] < numberOfLeafs[j];
585         };
586 
587     std::sort(children.begin(), children.end(), numberOfLeafsComparator);
588 
589     const  int numberOfChildren = children.size();
590     int small = 0;
591     int large = numberOfChildren - 1;
592     while (small < large) {
593         std::swap(children[small], children[large]);
594         small += 2;
595         large -= 2;
596     }
597 }
598 
599 /* Calculates a wedge angle that guarantees that no edge crosses can happen.
600  *
601  */
constrainWedgeAngle(const QVector<int> & numberOfLeafs,const qreal wedgeAngle,const qreal circleRadius,const int node,const QVector<int> & children,const qreal circleRadiusForChildren)602 qreal constrainWedgeAngle(const QVector<int>& numberOfLeafs, const qreal wedgeAngle,
603                           const qreal circleRadius, const int node,
604                           const QVector<int>& children, const qreal circleRadiusForChildren)
605 {
606     if (children.size() <= 1 or circleRadius == 0.) {
607         return wedgeAngle;
608     }
609 
610     //Proportion of the are in the sides of the wedge in which there will be no edges.
611     const qreal leftProportion = qreal(numberOfLeafs[children[0]]) / numberOfLeafs[node] / 2.;
612     const qreal rightProportion = qreal(numberOfLeafs[children.back()]) / numberOfLeafs[node] / 2.;
613     const qreal maximumProportion = qMax(leftProportion, rightProportion);
614 
615     //Limit the wedge angle to guarantee no crosses between edges.
616     const qreal maximumWedgeAngle = 2. * acos(circleRadius / circleRadiusForChildren) /
617                                     maximumProportion;
618 
619     return qMin(wedgeAngle, maximumWedgeAngle);
620 }
621 
622 /* Checks whether a given circle radius is valid for placing the children of a node.
623  *
624  * In order to be valid, the radius must be large enough to allow the radialLayoutHelper
625  * to place the node in such a way that guarantees that no edge crosses or node intersections
626  * can happen.
627  */
isValidCircleRadiusForChildren(const QVector<int> & numberOfLeafs,const qreal nodeRadius,const qreal wedgeAngle,const qreal circleRadius,const qreal nodeSeparation,const int node,const QVector<int> & children,const qreal circleRadiusForChildren)628 bool isValidCircleRadiusForChildren(const QVector<int>& numberOfLeafs, const qreal nodeRadius,
629                                     const qreal wedgeAngle, const qreal circleRadius,
630                                     const qreal nodeSeparation, const int node,
631                                     const QVector<int>& children,
632                                     const qreal circleRadiusForChildren)
633 {
634     const qreal constrainedWedgeAngle = constrainWedgeAngle(numberOfLeafs, wedgeAngle, circleRadius,
635                                                             node, children,
636                                                             circleRadiusForChildren);
637     const qreal minimumDistance = 2. * nodeRadius + nodeSeparation;
638     const qreal squaredMinimumDistance = minimumDistance * minimumDistance;
639 
640     const qreal nodeAngle = constrainedWedgeAngle / 2.;
641     QPointF nodePosition(circleRadius * qCos(nodeAngle), circleRadius * qSin(nodeAngle));
642 
643     const int numberOfChildren = children.size();
644     qreal childRotation = 0.;
645     QPointF previousChildPosition;
646     for (int i = 0; i < numberOfChildren; i++) {
647         const int child = children[i];
648         const qreal childWedgeAngle = constrainedWedgeAngle * numberOfLeafs[child] /
649                                       numberOfLeafs[node];
650         const qreal childAngle = childRotation + childWedgeAngle / 2.;
651         childRotation += childWedgeAngle;
652         QPointF childPosition(circleRadiusForChildren * qCos(childAngle),
653                               circleRadiusForChildren * qSin(childAngle));
654 
655         //Checks the distance between a node and of its children.
656         const QPointF nodeChildDifference = childPosition - nodePosition;
657         const qreal squaredNodeChildDistance(QPointF::dotProduct(nodeChildDifference,
658                                                                  nodeChildDifference));
659         if (squaredNodeChildDistance < squaredMinimumDistance) {
660             return false;
661         }
662 
663         //Checks the distance between two children.
664         if (i > 0) {
665             const QPointF difference = childPosition - previousChildPosition;
666             const qreal squaredDistanceBetweenChildren = QPointF::dotProduct(difference,
667                                                                              difference);
668             if (squaredDistanceBetweenChildren < squaredMinimumDistance) {
669                 return false;
670             }
671         }
672         previousChildPosition = childPosition;
673     }
674 
675 
676     return true;
677 }
678 
679 /* Calculates the radius of the circle at which the center of the children will be placed.
680  * outside of.
681  *
682  * This functions the minimum radius that guarantees that no edge crosses or circle intersections
683  * are possible, considering that a node and its children are placed by the radialLayoutHelper
684  * function.
685  */
calculateCircleRadiusForChildren(const QVector<int> & numberOfLeafs,const qreal nodeRadius,const qreal wedgeAngle,const qreal circleRadius,const qreal nodeSeparation,const int node,const QVector<int> & children)686 qreal calculateCircleRadiusForChildren(const QVector<int>& numberOfLeafs, const qreal nodeRadius,
687                                        const qreal wedgeAngle, const qreal circleRadius,
688                                        const qreal nodeSeparation, const int node,
689                                        const QVector<int>& children)
690 {
691     //Binary search is used to find the smallest valid radius up to a tolerance.
692     //In order to avoid infinite loop due to rounding errors, a maximum number of iterations
693     //is specified.
694     constexpr int MAXIMUM_NUMBER_OF_ITERATIONS = 100;
695     constexpr qreal TOLERANCE = 1.e-4;
696 
697     //Finds a suitable lower bound for the binary search
698     const qreal minimumDistanceBetweenCenters = 2. * nodeRadius + nodeSeparation;
699     qreal deltaRadiusLowerBound = qSqrt(qPow(minimumDistanceBetweenCenters, 2.) +
700                                         qPow(circleRadius, 2.)) - circleRadius;
701 
702     //Finds a suitable upper bound for the binary search.
703     qreal deltaRadiusUpperBound = deltaRadiusLowerBound;
704     for (int iteration = 0; iteration < MAXIMUM_NUMBER_OF_ITERATIONS; iteration++) {
705         const qreal circleRadiusForChildren = circleRadius + deltaRadiusUpperBound;
706         if (isValidCircleRadiusForChildren(numberOfLeafs, nodeRadius, wedgeAngle, circleRadius,
707                                            nodeSeparation, node, children,
708                                            circleRadiusForChildren)) {
709             break;
710         }
711 
712         deltaRadiusUpperBound *= 2.;
713     }
714 
715     //Searches for the minimum valid radius
716     for (int iteration = 0; iteration < MAXIMUM_NUMBER_OF_ITERATIONS; iteration++) {
717         if (deltaRadiusUpperBound - deltaRadiusLowerBound < TOLERANCE) {
718             break;
719         }
720 
721         const qreal deltaRadius = (deltaRadiusUpperBound + deltaRadiusLowerBound) / 2.;
722         const qreal circleRadiusForChildren = circleRadius + deltaRadius;
723         if (isValidCircleRadiusForChildren(numberOfLeafs, nodeRadius, wedgeAngle, circleRadius,
724                                            nodeSeparation, node, children,
725                                            circleRadiusForChildren)) {
726             deltaRadiusUpperBound = deltaRadius;
727         } else {
728             deltaRadiusLowerBound = deltaRadius;
729         }
730     }
731 
732     return circleRadius + deltaRadiusUpperBound;
733 }
734 
735 /* Helper function that calculates the radial layout recursively.
736  *
737  * This function calculates positions for each node in the sub-tree rooted at @p node.
738  * Consider the circle C centered at the origin with radius @circleRadius.
739  * Consider the wedge W, formed by the points with polar angle between @p rotationAngle and
740  * @p rotationAngle + @p wedgeAngle.
741  * The center of each node in the sub-tree rooted at @p node is placed outside or in the border of
742  * C and inside W. The parameters that define C and W are chosen in such a way that edge crosses
743  * are impossible and the distance between any pair of nodes is at least @p nodeSeparation.
744  *
745  * @param graph A tree graph.
746  * @param numberOfLeafs The number of leafs in the sub-tree rooted at each node.
747  * @param nodeRadius The radius of the circles used to draw nodes.
748  * @param wedgeAngle The internal angle of the wedge in which nodes are placed.
749  * @param rotationAngle The angle between the x-axis and the wedge in which nodes are placed.
750  * @param circleRadius The radius of the circles whose exterior is used to place nodes.
751  * @param nodeSeparation Lower bound on the distance between nodes
752  * @param node Root of the current sub-tree.
753  * @param visited Flags used to indicate which nodes have already been placed.
754  *                Initially, all positions of @p visited must be false.
755  *                This function changes @p visited.
756  * @param positions A place to write the position of each node.
757  */
radialLayoutHelper(const RemappedGraph & graph,const QVector<int> & numberOfLeafs,const qreal nodeRadius,const qreal wedgeAngle,const qreal rotationAngle,const qreal circleRadius,const qreal nodeSeparation,const int node,QVector<bool> & visited,QVector<QPointF> & positions)758 void radialLayoutHelper(const RemappedGraph& graph, const QVector<int>& numberOfLeafs,
759                         const qreal nodeRadius, const qreal wedgeAngle, const qreal rotationAngle,
760                         const qreal circleRadius, const qreal nodeSeparation, const int node,
761                         QVector<bool>& visited, QVector<QPointF>& positions)
762 {
763     visited[node] = true;
764 
765     //Places current node at the center of the wedge.
766     const qreal nodeAngle = wedgeAngle / 2. + rotationAngle;
767     positions[node].setX(circleRadius * qCos(nodeAngle));
768     positions[node].setY(circleRadius * qSin(nodeAngle));
769 
770     QVector<int> children = getChildren(graph, visited, node);
771     reorderChildrenForRadialLayout(numberOfLeafs, children);
772 
773     const qreal circleRadiusForChildren = calculateCircleRadiusForChildren(numberOfLeafs,
774             nodeRadius, wedgeAngle, circleRadius, nodeSeparation, node, children);
775 
776 
777     const qreal constrainedWedgeAngle = constrainWedgeAngle(numberOfLeafs, wedgeAngle, circleRadius,
778                                                             node, children,
779                                                             circleRadiusForChildren);
780 
781     qreal childRotationAngle = rotationAngle + (wedgeAngle - constrainedWedgeAngle) / 2.;
782     for (const int child : children) {
783         const qreal childWedgeAngle = constrainedWedgeAngle * numberOfLeafs[child] /
784                                       numberOfLeafs[node];
785 
786         radialLayoutHelper(graph, numberOfLeafs, nodeRadius, childWedgeAngle, childRotationAngle,
787                            circleRadiusForChildren, nodeSeparation, child, visited, positions);
788 
789         childRotationAngle += childWedgeAngle;
790     }
791 }
792 
793 /* Calculates the number of leafs in each sub-tree of a Depth-First-Search tree of @p graph.
794  *
795  * @param graph The graph to be searched.
796  * @param node The current node.
797  * @param visited Indicates which nodes have already been visited in the search.
798  * @param numberOfLeafs A place to store the answer.
799  */
calculateNumberOfLeafs(const RemappedGraph & graph,const int node,QVector<bool> & visited,QVector<int> & numberOfLeafs)800 void calculateNumberOfLeafs(const RemappedGraph& graph, const int node, QVector<bool>& visited,
801                             QVector<int>& numberOfLeafs)
802 {
803     visited[node] = true;
804 
805     numberOfLeafs[node] = 0;
806     bool isLeaf = true;
807     for (const int neighbour : graph.adjacency[node]) {
808         if (not visited[neighbour]) {
809             isLeaf = false;
810             calculateNumberOfLeafs(graph, neighbour, visited, numberOfLeafs);
811             numberOfLeafs[node] += numberOfLeafs[neighbour];
812         }
813     }
814 
815     if (isLeaf) {
816         numberOfLeafs[node] = 1;
817     }
818 }
819 
820 /* Finds a center of a tree.
821  *
822  * A center is a node with minimum eccentricity (i.e. distance to most distance node).
823  *
824  * @param graph A tree graph.
825  */
findTreeCenter(const RemappedGraph & graph)826 int findTreeCenter(const RemappedGraph& graph) {
827     int center = 0;
828     QQueue<int> queue;
829     QVector<int> degree(graph.numberOfNodes);
830     QVector<bool> visited(graph.numberOfNodes);
831     for (int node = 0; node < graph.numberOfNodes; node++) {
832         degree[node] = graph.adjacency[node].size();
833         if (degree[node] == 1) {
834             queue.enqueue(node);
835         }
836     }
837 
838     while (not queue.isEmpty()) {
839         const int node = queue.dequeue();
840         visited[node] = true;
841         center = node;
842         for (const int neighbour : graph.adjacency[node]) {
843             if (not visited[neighbour]) {
844                 degree[neighbour]--;
845                 if (degree[neighbour] == 1) {
846                     queue.enqueue(neighbour);
847                 }
848             }
849         }
850     }
851 
852     return center;
853 }
854 
855 /* Generates a radial layout for a tree.
856  *
857  * @param graph A tree graph.
858  * @param minX Minimum x-coordinate that can be assigned to a node.
859  * @param minY Minimum y-coordinate that can be assigned to a node.
860  * @param nodeRadius The radius of the circles used to draw nodes.
861  * @param nodeSeparation A lower bound on the distance between two nodes.
862  * @param root Root node of the tree.
863  * @param initialWedgeAngle Angle of the wedge used for the root.
864  * @param initialRotationAngle Rotation angle for the root.
865  */
radialLayout(const RemappedGraph & graph,const qreal minX,const qreal minY,const qreal nodeRadius,const qreal nodeSeparation,const int root,const qreal initialWedgeAngle,const qreal initialRotationAngle)866 QVector<QPointF> radialLayout(const RemappedGraph& graph, const qreal minX, const qreal minY,
867                               const qreal nodeRadius, const qreal nodeSeparation, const int root,
868                               const qreal initialWedgeAngle, const qreal initialRotationAngle)
869 {
870     QVector<bool> visited(graph.numberOfNodes);
871     QVector<int> numberOfLeafs(graph.numberOfNodes);
872     calculateNumberOfLeafs(graph, root, visited, numberOfLeafs);
873 
874     visited.fill(false);
875     QVector<QPointF> positions(graph.numberOfNodes);
876     radialLayoutHelper(graph, numberOfLeafs, nodeRadius, initialWedgeAngle, initialRotationAngle,
877                        0., nodeSeparation, root, visited, positions);
878 
879 
880     translateGraphToUpperLeftCorner(minX, qInf(), minY, qInf(), positions);
881 
882     return positions;
883 }
884 
885 /* Checks whether all the edges of a graph are bidirectional.
886  */
hasOnlyBidirectionalEdges(GraphDocumentPtr document)887 bool hasOnlyBidirectionalEdges(GraphDocumentPtr document)
888 {
889     const auto edges = document->edges();
890     for (const EdgePtr &edge : edges) {
891         if (edge->type()->direction() != EdgeType::Bidirectional) {
892             return false;
893         }
894     }
895     return true;
896 }
897 
898 /* Checks whether a graph is connected
899 */
isConnected(const RemappedGraph & graph)900 bool isConnected(const RemappedGraph& graph)
901 {
902     if (graph.numberOfNodes == 0) {
903         return true;
904     }
905 
906     QVector<bool> visited(graph.numberOfNodes);
907     QStack<int> stack;
908     stack.push(0);
909     visited[0] = true;
910     while (not stack.empty()) {
911         const int node = stack.pop();
912         for (const int neighbour : graph.adjacency[node]) {
913             if (not visited[neighbour]) {
914                 stack.push(neighbour);
915                 visited[neighbour] = true;
916             }
917         }
918     }
919 
920     for (int i = 0; i < graph.numberOfNodes; i++) {
921         if (not visited[i]) {
922             return false;
923         }
924     }
925 
926     return true;
927 }
928 
929 /* Checks whether a graph is a tree.
930 */
isTree(const RemappedGraph & graph)931 bool isTree(const RemappedGraph& graph)
932 {
933     if (graph.numberOfNodes - 1 != graph.edges.size()) {
934         return false;
935     }
936 
937     return isConnected(graph);
938 }
939 
applyRadialLayoutToTree(GraphDocumentPtr document,const qreal nodeRadius,const qreal margin,const qreal nodeSeparation,const NodePtr root,const qreal wedgeAngle,const qreal rotationAngle)940 bool Topology::applyRadialLayoutToTree(GraphDocumentPtr document, const qreal nodeRadius,
941                                        const qreal margin, const qreal nodeSeparation,
942                                        const NodePtr root, const qreal wedgeAngle,
943                                        const qreal rotationAngle)
944 {
945     //There is nothing to do with an empty graph.
946     if (document->nodes().empty()) {
947         return true;
948     }
949 
950     //Allow only bidirectional edges.
951     if (not hasOnlyBidirectionalEdges(document)) {
952         return false;
953     }
954 
955     const RemappedGraph graph = remapGraph(document);
956 
957     if (not isTree(graph)) {
958         return false;
959     }
960 
961     int rootIndex = 0;
962     if (root == nullptr) {
963         rootIndex = findTreeCenter(graph);
964     } else {
965         rootIndex = graph.nodeToIndexMap[root];
966     }
967 
968     const qreal minX = nodeRadius + margin;
969     const qreal minY = nodeRadius + margin;
970 
971     QVector<QPointF> positions = radialLayout(graph, minX, minY, nodeRadius, nodeSeparation,
972                                               rootIndex, wedgeAngle, rotationAngle);
973 
974     moveNodes(document->nodes(), graph.nodeToIndexMap, positions);
975 
976     return true;
977 }
978