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