Compare commits
4 Commits
d460c71a33
...
main
Author | SHA1 | Date | |
---|---|---|---|
b00dc96f5b
|
|||
6f0f5d05b6
|
|||
9f78c3e44a
|
|||
2d36d60020
|
@ -1,15 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "2.2.0"
|
kotlin("jvm") version "2.2.0"
|
||||||
|
id("org.jetbrains.dokka") version "2.0.0"
|
||||||
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.jaytux.altgraph"
|
group = "com.github.jaytux"
|
||||||
version = "1.0-SNAPSHOT"
|
version = "1.0-alpha"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(kotlin("stdlib"))
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,4 +22,23 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
jvmToolchain(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Jar>("dokkaJavadocJar") {
|
||||||
|
dependsOn(tasks.dokkaJavadoc)
|
||||||
|
from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
|
||||||
|
archiveClassifier.set("javadoc")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("maven") {
|
||||||
|
from(components["java"])
|
||||||
|
artifact(tasks.named("dokkaJavadocJar"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
65
src/main/kotlin/com/jaytux/altgraph/examples/SimpleRegDep.kt
Normal file
65
src/main/kotlin/com/jaytux/altgraph/examples/SimpleRegDep.kt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package com.jaytux.altgraph.examples
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.core.BaseGraph
|
||||||
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
import com.jaytux.altgraph.layout.PseudoForestLayout
|
||||||
|
import com.jaytux.altgraph.swing.DefaultVertexComponent
|
||||||
|
import com.jaytux.altgraph.swing.GraphPane
|
||||||
|
import com.jaytux.altgraph.swing.QuadraticEdge
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
data class Edge(val id: Int, val isWrite: Boolean)
|
||||||
|
fun getRegGraph(): IGraph<String, Edge> {
|
||||||
|
val graph = BaseGraph<String, Edge>()
|
||||||
|
val allowed = setOf(633, 631, 632, 634, 638, 635, 636)
|
||||||
|
val roots = listOf(647, 645, 643, 530, 519, 512, 513, 522, 523, 631, 632, 633, 655).sorted().filter { it in allowed }
|
||||||
|
val others = listOf(533, 550, 547, 552, 637, 638, 635, 634, 548, 659, 657, 636, 639, 557, 642, 640, 644, 646, 648, 650, 652, 653, 654, 656, 658).sorted().filter { it in allowed }
|
||||||
|
|
||||||
|
val regs = roots.associateWith { graph.addVertex("%reg$it", true) } + others.associateWith { graph.addVertex("%reg$it", false) }
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
val normal = listOf(
|
||||||
|
647 to 648, 645 to 646, 519 to 533, 519 to 557, 519 to 639, 512 to 550, 512 to 547, 513 to 552, 522 to 659, 522 to 657, 523 to 637, 631 to 638, 631 to 635,
|
||||||
|
632 to 634, 633 to 634, 633 to 636, 547 to 548, 637 to 659, 637 to 557, 637 to 639, 637 to 657, 635 to 636, 639 to 642, 639 to 640, 639 to 644, 646 to 648,
|
||||||
|
648 to 650, 639 to 648, 639 to 650, 639 to 653, 650 to 652, 650 to 652, 650 to 656, 650 to 654, 652 to 653, 654 to 656, 655 to 656, 650 to 658, 557 to 642,
|
||||||
|
557 to 654, 644 to 646, 657 to 658
|
||||||
|
).filter{ (f,s) -> f in allowed && s in allowed }.map { it to false }
|
||||||
|
val writes = listOf(
|
||||||
|
656 to 643, 550 to 519, 548 to 513, 552 to 522, 658 to 522, 637 to 522, 636 to 631, 634 to 631
|
||||||
|
).filter{ (f,s) -> f in allowed && s in allowed }.map { it to true }
|
||||||
|
(normal + writes).forEach { (p, isWrite) ->
|
||||||
|
val (from, to) = p
|
||||||
|
graph.connect(regs[from]!!, regs[to]!!, Edge(count++, isWrite))
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRegPane(graph: IGraph<String, Edge>): GraphPane<String, Edge> = GraphPane(graph) { pane, graph ->
|
||||||
|
PseudoForestLayout(graph, 10.0f, 20.0f, 10.0f, { it, p -> it.isWrite && p == PseudoForestLayout.LayoutPhase.LAYERING }) { v ->
|
||||||
|
(pane.getComponentFor(v) as DefaultVertexComponent).vertexSize()
|
||||||
|
}
|
||||||
|
}.also { pane ->
|
||||||
|
val edge = QuadraticEdge<Edge>(
|
||||||
|
delta = 0.0f,
|
||||||
|
color = { if(it.isWrite) Color.ORANGE.darker() else Color.BLACK }
|
||||||
|
)
|
||||||
|
pane.setEdgeRenderer(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val graph = getRegGraph()
|
||||||
|
val pane = getRegPane(graph)
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
val frame = JFrame("Simple Register Dependency Graph")
|
||||||
|
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
|
||||||
|
frame.setSize(800, 600)
|
||||||
|
frame.add(pane)
|
||||||
|
frame.pack()
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,15 @@ import kotlin.math.max
|
|||||||
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
|
* 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.
|
* 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
|
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
|
||||||
* [UnsupportedOperationException], and [unfreeze] is a no-op.
|
* [UnsupportedOperationException], and [unfreeze] is a no-op.
|
||||||
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
|
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
|
||||||
@ -33,6 +42,7 @@ import kotlin.math.max
|
|||||||
* @property horizontalMargin the horizontal margin between vertices in the same layer
|
* @property horizontalMargin the horizontal margin between vertices in the same layer
|
||||||
* @property disjoinXMargin the horizontal margin between disjoint subgraphs
|
* @property disjoinXMargin the horizontal margin between disjoint subgraphs
|
||||||
* @property interLayer the vertical margin between layers
|
* @property interLayer the vertical margin between layers
|
||||||
|
* @property ignoreInLayout a function that returns true if an edge should be ignored during layout (but are not inherently back-edges)
|
||||||
* @property vertexSize a function that returns the size of a vertex, including its label offset
|
* @property vertexSize a function that returns the size of a vertex, including its label offset
|
||||||
*
|
*
|
||||||
* @see ILayout
|
* @see ILayout
|
||||||
@ -42,9 +52,22 @@ class PseudoForestLayout<V, E>(
|
|||||||
var horizontalMargin: Float,
|
var horizontalMargin: Float,
|
||||||
var disjoinXMargin: Float,
|
var disjoinXMargin: Float,
|
||||||
var interLayer: Float,
|
var interLayer: Float,
|
||||||
|
val ignoreInLayout: (E, LayoutPhase) -> Boolean = { _, _ -> false },
|
||||||
vertexSize: (V) -> VertexSize
|
vertexSize: (V) -> VertexSize
|
||||||
) : ILayout<V, E>
|
) : ILayout<V, E>
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* An enum representing the different phases of the layout process where edges can be ignored.
|
||||||
|
* - [LAYERING]: during the layering phase, where back-edges are ignored to determine layers.
|
||||||
|
* - [DISJOINTS]: during the disjoint graph computation phase, where edges connecting disjoint subgraphs can be ignored.
|
||||||
|
* - [SLOT_ASSIGNMENT]: during the slot assignment phase, where certain edges may be ignored to optimize layout.
|
||||||
|
*/
|
||||||
|
enum class LayoutPhase {
|
||||||
|
LAYERING,
|
||||||
|
DISJOINTS,
|
||||||
|
SLOT_ASSIGNMENT
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class representing data on the size of a vertex, including its label offset and size.
|
* A class representing data on the size of a vertex, including its label offset and size.
|
||||||
*
|
*
|
||||||
@ -85,6 +108,7 @@ class PseudoForestLayout<V, E>(
|
|||||||
private val _positions = mutableMapOf<V, GPoint>()
|
private val _positions = mutableMapOf<V, GPoint>()
|
||||||
private var _vertexSize: (V) -> VertexSize = vertexSize
|
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||||
private var _boundingBox: GSize? = null
|
private var _boundingBox: GSize? = null
|
||||||
|
private var _repeat: Int = 3
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||||
@ -110,6 +134,20 @@ class PseudoForestLayout<V, E>(
|
|||||||
override fun graph(): IGraph<V, E> = _graph
|
override fun graph(): IGraph<V, E> = _graph
|
||||||
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = 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.
|
* Sets the vertex measuring function.
|
||||||
*
|
*
|
||||||
@ -117,56 +155,93 @@ class PseudoForestLayout<V, E>(
|
|||||||
*/
|
*/
|
||||||
fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } }
|
fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } }
|
||||||
|
|
||||||
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
private class Conn<V> private constructor(var from: Vert<V>?, var to: Vert<V>?, private val _id: Int) {
|
||||||
private data class Connector<V>(
|
constructor(from: Vert<V>?, to: Vert<V>?) : this(from, to, _nextId++) {}
|
||||||
var from: LayeredVertex<V>,
|
|
||||||
var to: LayeredVertex<V>
|
|
||||||
)
|
|
||||||
private data class LayeredVertex<V>(
|
|
||||||
val x: SumType<V, Connector<V>>
|
|
||||||
) {
|
|
||||||
constructor(x: V): this(x.sum1())
|
|
||||||
constructor(x: Connector<V>): this(x.sum2())
|
|
||||||
|
|
||||||
fun same(other: LayeredVertex<V>) = x.fold({ it1 ->
|
override fun equals(other: Any?): Boolean = other is Conn<*> && other._id == _id
|
||||||
|
override fun hashCode(): Int = _id.hashCode()
|
||||||
|
override fun toString(): String = "C[${from?.x?.fold({ it.toString() }) { "C" } ?: "null"}->${to?.x?.fold({ it.toString() }) { "C" } ?: "null"}@$_id]"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var _nextId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private class Vert<V>(val x: SumType<V, Conn<V>>) {
|
||||||
|
constructor(x: V): this(x.sum1())
|
||||||
|
constructor(x: Conn<V>): this(x.sum2())
|
||||||
|
|
||||||
|
override fun toString(): String = x.fold({ it.toString() }) { it.toString() }
|
||||||
|
|
||||||
|
fun same(other: Vert<V>) = x.fold({ it1 ->
|
||||||
other.x.fold({ it2 -> it1 == it2 }) { false }
|
other.x.fold({ it2 -> it1 == it2 }) { false }
|
||||||
}) { it1 ->
|
}) { it1 ->
|
||||||
other.x.fold({ false }) { it2 -> it1.from == it2.from && it1.to == it2.to }
|
other.x.fold({ false }) { it2 -> it1 == it2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// true is this is a direct parent of the other vertex/connector
|
fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
|
||||||
fun directParentOf(other: LayeredVertex<V>, graph: IGraph<V, *>): Boolean =
|
other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
|
||||||
x.fold({ xx ->
|
it2.from?.x == it1
|
||||||
// x is a vertex
|
|
||||||
other.x.fold({
|
|
||||||
// other is a vertex
|
|
||||||
graph.xToY(xx, it) != null
|
|
||||||
}) {
|
|
||||||
// other is a connector
|
|
||||||
it.from.same(this)
|
|
||||||
}
|
|
||||||
}) { xx ->
|
|
||||||
// x is a connector -> we need to connect to other
|
|
||||||
xx.to.same(other)
|
|
||||||
}
|
}
|
||||||
|
}) { it1 ->
|
||||||
|
it1.to!!.same(other)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private data class PreConnector<V>(
|
|
||||||
var x: SumType<LayeredVertex<V>, PreConnector<V>>?,
|
private fun buildChain(from: V, to: V, layerF: Int, layerT: Int): List<Vert<V>> {
|
||||||
var y: SumType<LayeredVertex<V>, PreConnector<V>>?
|
val first = Vert(from)
|
||||||
)
|
val last = Vert(to)
|
||||||
private fun realVertex(v: V) = LayeredVertex(v.sum1())
|
val chain = List(layerT - layerF - 1) { Vert(Conn<V>(null, null)) }
|
||||||
private fun buildChain(from: V, to: V, layerF: Int, layerT: Int): List<LayeredVertex<V>> {
|
|
||||||
val chain = mutableListOf(LayeredVertex(Connector(LayeredVertex(from), LayeredVertex(to))))
|
chain.forEachIndexed { i, v ->
|
||||||
while(chain.size < layerT - layerF - 1) {
|
if(i == 0) (v.x.asT2).from = first
|
||||||
val last = chain.last() // last is always Connector<V>, and last.to is always V (== to)
|
else (v.x.asT2).from = chain[i - 1]
|
||||||
val lastX = (last.x as SumType.SumT2<Connector<V>>).value
|
|
||||||
val next = LayeredVertex(Connector(last, lastX.to))
|
if(i == chain.size - 1) (v.x.asT2).to = last
|
||||||
lastX.to = next // reconnect
|
else (v.x.asT2).to = chain[i + 1]
|
||||||
chain += next
|
|
||||||
}
|
}
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reachableFrom(start: V, phase: LayoutPhase): Set<V> {
|
||||||
|
val seen = mutableSetOf<V>()
|
||||||
|
val queue = ArrayDeque<V>()
|
||||||
|
queue.add(start)
|
||||||
|
|
||||||
|
while(!queue.isEmpty()) {
|
||||||
|
val v = queue.removeFirst()
|
||||||
|
if(seen.add(v)) {
|
||||||
|
_graph.successors(v).forEach { (succ, edge) ->
|
||||||
|
if(ignoreInLayout(edge, phase)) return@forEach
|
||||||
|
if(succ !in seen) queue.addLast(succ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seen
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
|
||||||
|
var count = 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
conns.forEachIndexed { i, conn ->
|
||||||
|
for(j in i + 1 until conns.size) {
|
||||||
|
val other = conns[j]
|
||||||
|
|
||||||
|
if(conn.first == other.first || conn.second == other.second) continue // shared vertex -> cannot cross
|
||||||
|
|
||||||
|
val topFirst = conn.first < other.first
|
||||||
|
val botFirst = conn.second < other.second
|
||||||
|
|
||||||
|
if(topFirst != botFirst) count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
override fun compute() {
|
override fun compute() {
|
||||||
println("Acquiring lock")
|
println("Acquiring lock")
|
||||||
locked {
|
locked {
|
||||||
@ -182,20 +257,20 @@ class PseudoForestLayout<V, E>(
|
|||||||
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||||
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
||||||
queue.addAll(roots.map { it to emptySet() })
|
queue.addAll(roots.map { it to emptySet() })
|
||||||
println(" - Computing layers from roots: $roots")
|
|
||||||
|
|
||||||
while (!queue.isEmpty()) {
|
while (!queue.isEmpty()) {
|
||||||
val (vertex, onPath) = queue.removeFirst()
|
val (vertex, onPath) = queue.removeFirst()
|
||||||
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||||
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
|
|
||||||
|
|
||||||
val succLayer = layer + 1
|
val succLayer = layer + 1
|
||||||
_graph.successors(vertex).forEach { (succ, _) ->
|
_graph.successors(vertex).forEach { (succ, edge) ->
|
||||||
|
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
if (succ in onPath) return@forEach
|
if (succ in onPath) return@forEach
|
||||||
dep += succ
|
dep += succ
|
||||||
|
|
||||||
layers[succ]?.let { (l, sDep) ->
|
layers[succ]?.let { (l, sDep) ->
|
||||||
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
|
|
||||||
dep += sDep
|
dep += sDep
|
||||||
|
|
||||||
val delta = succLayer - l
|
val delta = succLayer - l
|
||||||
@ -209,15 +284,12 @@ class PseudoForestLayout<V, E>(
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
layers[succ] = succLayer to mutableSetOf()
|
layers[succ] = succLayer to mutableSetOf()
|
||||||
}
|
}
|
||||||
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
|
|
||||||
queue.addLast(succ to onPath + vertex)
|
queue.addLast(succ to onPath + vertex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure dependents are always up to date
|
// ensure dependents are always up to date
|
||||||
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
|
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
|
// Cache node sizes
|
||||||
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
||||||
@ -225,7 +297,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
// Compute layer y positions (and thus the bounding box height).
|
// Compute layer y positions (and thus the bounding box height).
|
||||||
val layerCount = layers.maxOf { it.value.first } + 1
|
val layerCount = layers.maxOf { it.value.first } + 1
|
||||||
val layerHeights = MutableList(layerCount) { 0.0f }
|
val layerHeights = MutableList(layerCount) { 0.0f }
|
||||||
println(" - Have $layerCount layers")
|
|
||||||
|
|
||||||
var minOffset = Float.POSITIVE_INFINITY
|
var minOffset = Float.POSITIVE_INFINITY
|
||||||
var maxOffset = Float.NEGATIVE_INFINITY
|
var maxOffset = Float.NEGATIVE_INFINITY
|
||||||
@ -248,18 +319,9 @@ class PseudoForestLayout<V, E>(
|
|||||||
y
|
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
|
// Compute disjoint graphs
|
||||||
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||||
val reachable = layers[root]?.second?.toMutableSet() ?: return@fold acc
|
val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
|
||||||
reachable += root
|
|
||||||
|
|
||||||
val dedup = acc.mapNotNull { other ->
|
val dedup = acc.mapNotNull { other ->
|
||||||
val inter = reachable intersect other
|
val inter = reachable intersect other
|
||||||
if(inter.isEmpty()) other // fully disjoint -> keep
|
if(inter.isEmpty()) other // fully disjoint -> keep
|
||||||
@ -274,18 +336,14 @@ class PseudoForestLayout<V, E>(
|
|||||||
}
|
}
|
||||||
var currentXZero = 0.0f
|
var currentXZero = 0.0f
|
||||||
disjoint.forEach { sub ->
|
disjoint.forEach { sub ->
|
||||||
println(" - Layouting disjoint subgraph: $sub")
|
|
||||||
// Put each vertex in a list by layer
|
// Put each vertex in a list by layer
|
||||||
val layered = List(layerCount) { layer ->
|
val layered = List(layerCount) { layer ->
|
||||||
sub.mapNotNull {
|
sub.mapNotNull {
|
||||||
if(layers[it]?.first == layer) realVertex(it)
|
if(layers[it]?.first == layer) Vert(it)
|
||||||
else null
|
else null
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
println(" - Initial layered vertices:")
|
|
||||||
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
|
|
||||||
|
|
||||||
// Break up multi-layer edges with dummy nodes
|
// Break up multi-layer edges with dummy nodes
|
||||||
layered.forEachIndexed { idx, list ->
|
layered.forEachIndexed { idx, list ->
|
||||||
list.forEach { v ->
|
list.forEach { v ->
|
||||||
@ -299,6 +357,12 @@ class PseudoForestLayout<V, E>(
|
|||||||
layered[idx + offset + 1] += dummy
|
layered[idx + offset + 1] += dummy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(otherLayer < idx - 1) {
|
||||||
|
val chain = buildChain(other, node, otherLayer, idx)
|
||||||
|
chain.forEachIndexed { offset, dummy ->
|
||||||
|
layered[otherLayer + offset + 1] += dummy
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) {} // do nothing on dummy nodes
|
}) {} // do nothing on dummy nodes
|
||||||
}
|
}
|
||||||
@ -317,7 +381,7 @@ class PseudoForestLayout<V, E>(
|
|||||||
// Layer-by-layer, assign x slots (not yet positions)
|
// Layer-by-layer, assign x slots (not yet positions)
|
||||||
for(i in 1 until layered.size) {
|
for(i in 1 until layered.size) {
|
||||||
// Barycenter heuristic: average of parents' slots
|
// Barycenter heuristic: average of parents' slots
|
||||||
val heuristic = { v: LayeredVertex<V> ->
|
val heuristic = { v: Vert<V> ->
|
||||||
val parents = layered[i - 1].mapIndexedNotNull { idx, p -> if(p.directParentOf(v, _graph)) idx.toFloat() else null }
|
val parents = layered[i - 1].mapIndexedNotNull { idx, p -> if(p.directParentOf(v, _graph)) idx.toFloat() else null }
|
||||||
parents.sum() / parents.size
|
parents.sum() / parents.size
|
||||||
}
|
}
|
||||||
@ -335,6 +399,48 @@ class PseudoForestLayout<V, E>(
|
|||||||
val maxWidth = layerWidths.max()
|
val maxWidth = layerWidths.max()
|
||||||
|
|
||||||
// TODO: do some reorderings to minimize #crossings?
|
// TODO: do some reorderings to minimize #crossings?
|
||||||
|
println(" - Optimizing slot assignments")
|
||||||
|
layered.forEachIndexed { idx, list ->
|
||||||
|
println(" - Layer $idx: $list")
|
||||||
|
}
|
||||||
|
|
||||||
|
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
|
||||||
|
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 {
|
||||||
|
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
|
||||||
|
}.map { it to vBot }
|
||||||
|
current.addAll(forVBot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Assign x positions
|
||||||
layered.forEachIndexed { idx, layer ->
|
layered.forEachIndexed { idx, layer ->
|
||||||
|
@ -59,6 +59,18 @@ sealed class SumType<out T1, out T2> {
|
|||||||
is SumT2 -> onT2(value)
|
is SumT2 -> onT2(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the contained value as a `T1`, or throws if the sum type holds a `T2`.
|
||||||
|
*/
|
||||||
|
val asT1: T1
|
||||||
|
get() = fold({ it }) { throw IllegalStateException("Not a T1: $this") }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the contained value as a `T2`, or throws if the sum type holds a `T1`.
|
||||||
|
*/
|
||||||
|
val asT2: T2
|
||||||
|
get() = fold({ throw IllegalStateException("Not a T2: $this") }) { it }
|
||||||
|
|
||||||
override fun toString(): String = fold({ it.toString() }) { it.toString() }
|
override fun toString(): String = fold({ it.toString() }) { it.toString() }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,12 +13,12 @@ import kotlin.math.sqrt
|
|||||||
class QuadraticEdge<E>(
|
class QuadraticEdge<E>(
|
||||||
var delta: Float = 0.2f,
|
var delta: Float = 0.2f,
|
||||||
var arrowLen: Int = 10,
|
var arrowLen: Int = 10,
|
||||||
var color: Color = Color.BLACK,
|
var color: (E) -> Color = { Color.BLACK },
|
||||||
var arrowAngle: Double = Math.PI / 6.0
|
var arrowAngle: Double = Math.PI / 6.0
|
||||||
) : IEdgeRenderer<E> {
|
) : IEdgeRenderer<E> {
|
||||||
override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) {
|
override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) {
|
||||||
val g2 = g.create() as Graphics2D
|
val g2 = g.create() as Graphics2D
|
||||||
g2.color = color
|
g2.color = color(meta)
|
||||||
|
|
||||||
val len = from.distance(to).toFloat()
|
val len = from.distance(to).toFloat()
|
||||||
val xLen = delta * len
|
val xLen = delta * len
|
||||||
|
Reference in New Issue
Block a user