diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt b/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt index 97a75e2..087853f 100644 --- a/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt +++ b/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt @@ -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( private val _positions = mutableMapOf() 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( override fun graph(): IGraph = _graph override fun setGraph(graph: IGraph) { 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( fun directParentOf(other: Vert, graph: IGraph): 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( 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( private fun computeCrossings(lTop: List>, lBot: List>, edges: List, Vert>>): 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( 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( val layers = mutableMapOf>>() val queue = ArrayDeque>>(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( } ?: 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( // 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( 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>()) { acc, root -> val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet() @@ -340,7 +336,6 @@ class PseudoForestLayout( } 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( }.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( } val edges = mutableListOf, Vert>>>() - val crossings = mutableListOf() 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>()) { (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>()) { (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 diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/Util.kt b/src/main/kotlin/com/jaytux/altgraph/layout/Util.kt new file mode 100644 index 0000000..ccb7993 --- /dev/null +++ b/src/main/kotlin/com/jaytux/altgraph/layout/Util.kt @@ -0,0 +1,13 @@ +package com.jaytux.altgraph.layout + +fun List.permutations(): Sequence> = 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) + } + } + } +} \ No newline at end of file