Improved layouting (minimize #crossings)
This commit is contained in:
@ -14,6 +14,15 @@ import kotlin.math.max
|
||||
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
|
||||
* It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout.
|
||||
*
|
||||
* You can improve the layout by increasing the iteration count via [setIterationCount], at the cost of longer
|
||||
* computation time (default: 3).
|
||||
*
|
||||
* Additionally, you can ignore certain edges during layout by providing a function to [ignoreInLayout]; during each
|
||||
* of the phases where edges can be ignored ([LayoutPhase.LAYERING] (deciding layers for each vertex),
|
||||
* [LayoutPhase.DISJOINTS] (deciding which sub-graphs to consider disjoint), and [LayoutPhase.SLOT_ASSIGNMENT] (deciding
|
||||
* the order of vertices in each layer)), the function will be called for each edge (once), and if it returns true,
|
||||
* the edge will be ignored for that phase. By default, no edges are ignored.
|
||||
*
|
||||
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
|
||||
* [UnsupportedOperationException], and [unfreeze] is a no-op.
|
||||
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
|
||||
@ -99,6 +108,7 @@ class PseudoForestLayout<V, E>(
|
||||
private val _positions = mutableMapOf<V, GPoint>()
|
||||
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||
private var _boundingBox: GSize? = null
|
||||
private var _repeat: Int = 3
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||
@ -124,6 +134,20 @@ class PseudoForestLayout<V, E>(
|
||||
override fun graph(): IGraph<V, E> = _graph
|
||||
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
|
||||
|
||||
/**
|
||||
* Gets the number of iterations the layout algorithm will perform to optimize the layout.
|
||||
*
|
||||
* @return the number of iterations
|
||||
*/
|
||||
fun getIterationCount(): Int = _repeat
|
||||
/**
|
||||
* Sets the number of iterations the layout algorithm will perform to optimize the layout.
|
||||
* More iterations may yield a better layout, but will take longer to compute.
|
||||
*
|
||||
* @param repeat the number of iterations
|
||||
*/
|
||||
fun setIterationCount(repeat: Int) { locked { _repeat = repeat } }
|
||||
|
||||
/**
|
||||
* Sets the vertex measuring function.
|
||||
*
|
||||
@ -156,7 +180,6 @@ class PseudoForestLayout<V, E>(
|
||||
|
||||
fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
|
||||
other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
|
||||
println(" - Checking direct parenthood between $it1 and dummy $it2 (${it2.from}): ${it2.from?.x == it1}")
|
||||
it2.from?.x == it1
|
||||
}
|
||||
}) { it1 ->
|
||||
@ -176,8 +199,6 @@ class PseudoForestLayout<V, E>(
|
||||
if(i == chain.size - 1) (v.x.asT2).to = last
|
||||
else (v.x.asT2).to = chain[i + 1]
|
||||
}
|
||||
|
||||
println(" - Breaking edge $from -> $to with chain: $chain")
|
||||
return chain
|
||||
}
|
||||
|
||||
@ -201,17 +222,10 @@ class PseudoForestLayout<V, E>(
|
||||
|
||||
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
|
||||
var count = 0
|
||||
|
||||
println(" - Computing crossings between layers:")
|
||||
println(" - Top: $lTop")
|
||||
println(" - Bot: $lBot")
|
||||
println(" - Edges: $edges")
|
||||
|
||||
val conns = edges.map { (top, bot) -> lTop.indexOf(top) to lBot.indexOf(bot) }.sortedWith { (t1, b1), (t2, b2) ->
|
||||
if (t1 != t2) t1 - t2
|
||||
else b1 - b2
|
||||
}
|
||||
println(" - Connections (by index): $conns")
|
||||
|
||||
conns.forEachIndexed { i, conn ->
|
||||
for(j in i + 1 until conns.size) {
|
||||
@ -222,10 +236,7 @@ class PseudoForestLayout<V, E>(
|
||||
val topFirst = conn.first < other.first
|
||||
val botFirst = conn.second < other.second
|
||||
|
||||
if(topFirst != botFirst) {
|
||||
println(" - Crossing between ${lTop[conn.first]}->${lBot[conn.second]} and ${lTop[other.first]}->${lBot[other.second]}")
|
||||
count++
|
||||
}
|
||||
if(topFirst != botFirst) count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
@ -246,24 +257,20 @@ class PseudoForestLayout<V, E>(
|
||||
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
||||
queue.addAll(roots.map { it to emptySet() })
|
||||
println(" - Computing layers from roots: $roots")
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
val (vertex, onPath) = queue.removeFirst()
|
||||
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
|
||||
|
||||
val succLayer = layer + 1
|
||||
_graph.successors(vertex).forEach { (succ, edge) ->
|
||||
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
|
||||
println(" - Ignoring edge $edge for layout")
|
||||
return@forEach
|
||||
}
|
||||
if (succ in onPath) return@forEach
|
||||
dep += succ
|
||||
|
||||
layers[succ]?.let { (l, sDep) ->
|
||||
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
|
||||
dep += sDep
|
||||
|
||||
val delta = succLayer - l
|
||||
@ -277,15 +284,12 @@ class PseudoForestLayout<V, E>(
|
||||
} ?: run {
|
||||
layers[succ] = succLayer to mutableSetOf()
|
||||
}
|
||||
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
|
||||
queue.addLast(succ to onPath + vertex)
|
||||
}
|
||||
|
||||
// ensure dependents are always up to date
|
||||
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
|
||||
}
|
||||
println(" - Assigned layers:")
|
||||
layers.forEach { (v, p) -> println(" - Vertex $v: layer ${p.first}, dependents: ${p.second}") }
|
||||
|
||||
// Cache node sizes
|
||||
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
||||
@ -293,7 +297,6 @@ class PseudoForestLayout<V, E>(
|
||||
// Compute layer y positions (and thus the bounding box height).
|
||||
val layerCount = layers.maxOf { it.value.first } + 1
|
||||
val layerHeights = MutableList(layerCount) { 0.0f }
|
||||
println(" - Have $layerCount layers")
|
||||
|
||||
var minOffset = Float.POSITIVE_INFINITY
|
||||
var maxOffset = Float.NEGATIVE_INFINITY
|
||||
@ -316,13 +319,6 @@ class PseudoForestLayout<V, E>(
|
||||
y
|
||||
}
|
||||
|
||||
println(" - Layer measurements: (height, y) = ${(layerHeights zip layerY)}")
|
||||
println(" - Layers with nodes:")
|
||||
for(i in 0 until layerCount) {
|
||||
val verts = layers.filter { it.value.first == i }.keys
|
||||
println(" - Layer $i: $verts")
|
||||
}
|
||||
|
||||
// Compute disjoint graphs
|
||||
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||
val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
|
||||
@ -340,7 +336,6 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
var currentXZero = 0.0f
|
||||
disjoint.forEach { sub ->
|
||||
println(" - Layouting disjoint subgraph: $sub")
|
||||
// Put each vertex in a list by layer
|
||||
val layered = List(layerCount) { layer ->
|
||||
sub.mapNotNull {
|
||||
@ -349,9 +344,6 @@ class PseudoForestLayout<V, E>(
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
println(" - Initial layered vertices:")
|
||||
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
|
||||
|
||||
// Break up multi-layer edges with dummy nodes
|
||||
layered.forEachIndexed { idx, list ->
|
||||
list.forEach { v ->
|
||||
@ -413,22 +405,41 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
|
||||
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
|
||||
val crossings = mutableListOf<Int>()
|
||||
for(i in 1 until layered.size) {
|
||||
edges.add(mutableListOf())
|
||||
val current = edges[i - 1]
|
||||
|
||||
layered[i].forEach { vBot ->
|
||||
val forVBot = layered[i - 1].filter {
|
||||
println(" - Checking edge between $it and $vBot: ${it.directParentOf(vBot, _graph)} || ${vBot.directParentOf(it, _graph)}")
|
||||
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
|
||||
}.map { it to vBot }
|
||||
current.addAll(forVBot)
|
||||
}
|
||||
}
|
||||
|
||||
val c = computeCrossings(layered[i - 1], layered[i], current)
|
||||
crossings.add(c)
|
||||
println(" - Connections between layer ${i - 1} and $i have $c crossings")
|
||||
repeat(_repeat) {
|
||||
val optP0 = layered[0].permutations().map { perm ->
|
||||
val crossings = computeCrossings(perm, layered[1], edges[0])
|
||||
println(" - Layer 0 permutation $perm (to ${layered[1]} has $crossings crossings")
|
||||
perm to crossings
|
||||
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
|
||||
if (accCost > cost) cost.toFloat() to perm
|
||||
else accCost to accSeq
|
||||
}
|
||||
layered[0].clear()
|
||||
layered[0].addAll(optP0.second.toMutableList())
|
||||
for (i in 1 until layered.size) {
|
||||
val optPi = layered[i].permutations().map { perm ->
|
||||
val crossings = computeCrossings(layered[i - 1], perm, edges[i - 1])
|
||||
println(" - Layer $i permutation $perm (from ${layered[i - 1]} has $crossings crossings")
|
||||
perm to crossings
|
||||
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
|
||||
if (accCost > cost) cost.toFloat() to perm
|
||||
else accCost to accSeq
|
||||
}
|
||||
layered[i].clear()
|
||||
layered[i].addAll(optPi.second.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
// Assign x positions
|
||||
|
13
src/main/kotlin/com/jaytux/altgraph/layout/Util.kt
Normal file
13
src/main/kotlin/com/jaytux/altgraph/layout/Util.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package com.jaytux.altgraph.layout
|
||||
|
||||
fun <T> List<T>.permutations(): Sequence<List<T>> = sequence {
|
||||
if(isEmpty()) yield(emptyList())
|
||||
else {
|
||||
for(i in indices) {
|
||||
val elem = this@permutations[i]
|
||||
for(perm in (this@permutations - elem).permutations()) {
|
||||
yield(listOf(elem) + perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user