Compare commits
6 Commits
99d64242cf
...
1.0-alpha
Author | SHA1 | Date | |
---|---|---|---|
6f0f5d05b6
|
|||
9f78c3e44a
|
|||
2d36d60020
|
|||
d460c71a33
|
|||
4a5db84148
|
|||
64560b172b
|
@ -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 = "be.topl.phoenix-intellij"
|
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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,3 +23,22 @@ 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,19 @@
|
|||||||
package com.jaytux.altgraph.core
|
package com.jaytux.altgraph.core
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mutable directed graph implementation, with arbitrary vertex and edge types. Supports marking vertices as "roots"
|
||||||
|
* during addition.
|
||||||
|
*
|
||||||
|
* Vertices and edges must be unique (no duplicates allowed). Edges are directed, and there can be at most one edge
|
||||||
|
* from a given vertex `A` to another vertex `B`.
|
||||||
|
*
|
||||||
|
* All operations that attempt to violate these constraints will throw a [GraphException].
|
||||||
|
*
|
||||||
|
* @param V The vertex type.
|
||||||
|
* @param E The edge type.
|
||||||
|
* @see [IMutableGraph]
|
||||||
|
* @see [IGraph]
|
||||||
|
*/
|
||||||
open class BaseGraph<V, E> : IMutableGraph<V, E> {
|
open class BaseGraph<V, E> : IMutableGraph<V, E> {
|
||||||
private val _vertices = mutableMapOf<V, Boolean>()
|
private val _vertices = mutableMapOf<V, Boolean>()
|
||||||
// [from] -> {e: exists v s.t. e = (from, v)}
|
// [from] -> {e: exists v s.t. e = (from, v)}
|
||||||
@ -70,6 +84,15 @@ open class BaseGraph<V, E> : IMutableGraph<V, E> {
|
|||||||
if (_existing[from]?.isEmpty() == true) { _existing.remove(from) }
|
if (_existing[from]?.isEmpty() == true) { _existing.remove(from) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an immutable view of this graph.
|
||||||
|
*
|
||||||
|
* Further modifications to the original graph will be reflected in the immutable view.
|
||||||
|
*
|
||||||
|
* This operations is very cheap, as it does not involve any copying of data.
|
||||||
|
*
|
||||||
|
* @return An immutable view of this graph
|
||||||
|
*/
|
||||||
fun immutable(): IGraph<V, E> = BaseGraphImmutable()
|
fun immutable(): IGraph<V, E> = BaseGraphImmutable()
|
||||||
|
|
||||||
override fun successors(vertex: V): Map<V, E> =
|
override fun successors(vertex: V): Map<V, E> =
|
||||||
|
@ -1,42 +1,221 @@
|
|||||||
package com.jaytux.altgraph.core
|
package com.jaytux.altgraph.core
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple, immutable graph interface.
|
||||||
|
*
|
||||||
|
* The graph is directed, and may contain cycles (including self-loops). However, it may not contain parallel edges.
|
||||||
|
*
|
||||||
|
* The supported operations are:
|
||||||
|
* - Querying for vertices and edges;
|
||||||
|
* - Querying for graph roots (these do not have to be roots in the traditional sense, but can be any vertex that
|
||||||
|
* should be treated as a root for layout purposes);
|
||||||
|
* - Querying for successors and predecessors of a vertex;
|
||||||
|
* - Querying for the edge between two vertices, if it exists.
|
||||||
|
*
|
||||||
|
* Importantly, each vertex (`V`) and edge (`E`) should be unique in the graph. Edges should not encode any incidence
|
||||||
|
* information (i.e. their endpoints), but should be distinct objects.
|
||||||
|
*
|
||||||
|
* Mutable graphs should implement [IMutableGraph], which extends this interface with mutation operations.
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param E the type of edges in the graph
|
||||||
|
* @see IMutableGraph
|
||||||
|
* @see BaseGraph
|
||||||
|
*/
|
||||||
interface IGraph<V, E> {
|
interface IGraph<V, E> {
|
||||||
|
/**
|
||||||
|
* Gets all the vertices in the graph.
|
||||||
|
*
|
||||||
|
* @return a set of all vertices in the graph
|
||||||
|
*/
|
||||||
fun vertices(): Set<V>
|
fun vertices(): Set<V>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the edges in the graph, along with their endpoints.
|
||||||
|
*
|
||||||
|
* @return a map from edges to their endpoints (as pairs (from, to))
|
||||||
|
*/
|
||||||
fun edges(): Map<E, Pair<V, V>>
|
fun edges(): Map<E, Pair<V, V>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the root vertices in the graph.
|
||||||
|
*
|
||||||
|
* @return a set of all root vertices in the graph
|
||||||
|
*/
|
||||||
fun roots(): Set<V>
|
fun roots(): Set<V>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the successors of a given vertex, along with the connecting edges.
|
||||||
|
*
|
||||||
|
* A default implementation is provided in the [IGraph] interface, but may be overridden for efficiency.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex whose successors are to be found
|
||||||
|
* @return a map from successor vertices to their outgoing edges
|
||||||
|
* @see predecessors
|
||||||
|
*/
|
||||||
fun successors(vertex: V): Map<V, E> =
|
fun successors(vertex: V): Map<V, E> =
|
||||||
edges().filter { it.value.first == vertex }
|
edges().filter { it.value.first == vertex }
|
||||||
.map { (edge, pair) -> pair.second to edge }.toMap()
|
.map { (edge, pair) -> pair.second to edge }.toMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the predecessors of a given vertex, along with the connecting edges.
|
||||||
|
*
|
||||||
|
* A default implementation is provided in the [IGraph] interface, but may be overridden for efficiency.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex whose predecessors are to be found
|
||||||
|
* @return a map from predecessor vertices to their incoming edges
|
||||||
|
* @see successors
|
||||||
|
*/
|
||||||
fun predecessors(vertex: V): Map<V, E> =
|
fun predecessors(vertex: V): Map<V, E> =
|
||||||
edges().filter { it.value.second == vertex }
|
edges().filter { it.value.second == vertex }
|
||||||
.map { (edge, pair) -> pair.first to edge }.toMap()
|
.map { (edge, pair) -> pair.first to edge }.toMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an edge exists between two vertices, and returns it if so.
|
||||||
|
*
|
||||||
|
* A default implementation is provided in the [IGraph] interface, but may be overridden for efficiency.
|
||||||
|
*
|
||||||
|
* @param x the starting vertex
|
||||||
|
* @param y the ending vertex
|
||||||
|
* @return the edge from `x` to `y`, or `null` if no such edge exists
|
||||||
|
*/
|
||||||
fun xToY(x: V, y: V): E? = edges().entries.firstOrNull { it.value == x to y }?.key
|
fun xToY(x: V, y: V): E? = edges().entries.firstOrNull { it.value == x to y }?.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple, mutable graph interface, extending [IGraph].
|
||||||
|
*
|
||||||
|
* In addition to the operations provided by [IGraph], mutable graphs support:
|
||||||
|
* - Adding and removing vertices;
|
||||||
|
* - Connecting and disconnecting vertices with edges.
|
||||||
|
*
|
||||||
|
* The same restrictions apply as for [IGraph]: the graph is directed, can contain cycles (including self-loops), can't
|
||||||
|
* contain parallel edges, and vertex (`V`) and edge (`E`) objects should be unique in the graph.
|
||||||
|
*
|
||||||
|
* Immutable graphs should implement [IGraph].
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param E the type of edges in the graph
|
||||||
|
* @see IGraph
|
||||||
|
* @see BaseGraph
|
||||||
|
*/
|
||||||
interface IMutableGraph<V, E> : IGraph<V, E> {
|
interface IMutableGraph<V, E> : IGraph<V, E> {
|
||||||
|
/**
|
||||||
|
* Adds a vertex to the graph.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex to add
|
||||||
|
* @param isRoot whether the vertex should be considered a root (by default, `false`)
|
||||||
|
* @return the added vertex
|
||||||
|
* @throws GraphException if the vertex already exists in the graph
|
||||||
|
*/
|
||||||
fun addVertex(vertex: V, isRoot: Boolean = false): V
|
fun addVertex(vertex: V, isRoot: Boolean = false): V
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a vertex from the graph.
|
||||||
|
*
|
||||||
|
* If the vertex has any incident edges (i.e. incoming or outgoing edges), they are also removed.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex to remove
|
||||||
|
* @throws GraphException if the vertex does not exist in the graph
|
||||||
|
*/
|
||||||
fun removeVertex(vertex: V)
|
fun removeVertex(vertex: V)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects two vertices with an edge.
|
||||||
|
*
|
||||||
|
* @param from the starting vertex
|
||||||
|
* @param to the ending vertex
|
||||||
|
* @param edge the edge to add between `from` and `to`
|
||||||
|
* @return the added edge
|
||||||
|
* @throws GraphException if either vertex does not exist in the graph, if the edge already exists in the graph,
|
||||||
|
* or if there is already an edge between `from` and `to`.
|
||||||
|
*/
|
||||||
fun connect(from: V, to: V, edge: E): E
|
fun connect(from: V, to: V, edge: E): E
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects two vertices by removing the edge between them.
|
||||||
|
*
|
||||||
|
* @param from the starting vertex
|
||||||
|
* @param to the ending vertex
|
||||||
|
* @throws GraphException if there is no edge between `from` and `to`
|
||||||
|
*/
|
||||||
fun disconnect(from: V, to: V)
|
fun disconnect(from: V, to: V)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an edge from the graph.
|
||||||
|
*
|
||||||
|
* @param edge the edge to remove
|
||||||
|
* @throws GraphException if the edge does not exist in the graph
|
||||||
|
*/
|
||||||
fun removeEdge(edge: E)
|
fun removeEdge(edge: E)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a graph operation fails.
|
||||||
|
*
|
||||||
|
* @param message the exception message
|
||||||
|
*/
|
||||||
class GraphException(message: String) : RuntimeException(message) {
|
class GraphException(message: String) : RuntimeException(message) {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that a vertex already exists in the graph.
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param vertex the vertex that already exists
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <V> vertexAlreadyExists(vertex: V) =
|
fun <V> vertexAlreadyExists(vertex: V) =
|
||||||
GraphException("Vertex '$vertex' already exists in this graph.")
|
GraphException("Vertex '$vertex' already exists in this graph.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that an edge already exists in the graph.
|
||||||
|
*
|
||||||
|
* @param E the type of edges in the graph
|
||||||
|
* @param edge the edge that already exists
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <E> edgeAlreadyExists(edge: E) =
|
fun <E> edgeAlreadyExists(edge: E) =
|
||||||
GraphException("Edge '$edge' already exists in this graph.")
|
GraphException("Edge '$edge' already exists in this graph.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that an edge already exists between two vertices in the graph.
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param from the starting vertex
|
||||||
|
* @param to the ending vertex
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <V> edgeBetweenAlreadyExists(from: V, to: V) =
|
fun <V> edgeBetweenAlreadyExists(from: V, to: V) =
|
||||||
GraphException("Edge from '$from' to '$to' already exists in this graph.")
|
GraphException("Edge from '$from' to '$to' already exists in this graph.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that a vertex was not found in the graph.
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param vertex the vertex that was not found
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <V> vertexNotFound(vertex: V) =
|
fun <V> vertexNotFound(vertex: V) =
|
||||||
GraphException("Vertex '$vertex' not found in this graph.")
|
GraphException("Vertex '$vertex' not found in this graph.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that no edge was found between two vertices in the graph.
|
||||||
|
*
|
||||||
|
* @param V the type of vertices in the graph
|
||||||
|
* @param from the starting vertex
|
||||||
|
* @param to the ending vertex
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <V> noEdgeFound(from: V, to: V) =
|
fun <V> noEdgeFound(from: V, to: V) =
|
||||||
GraphException("No edge found from '$from' to '$to' in this graph.")
|
GraphException("No edge found from '$from' to '$to' in this graph.")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [GraphException] indicating that an edge was not found in the graph.
|
||||||
|
*
|
||||||
|
* @param E the type of edges in the graph
|
||||||
|
* @param edge the edge that was not found
|
||||||
|
* @return a [GraphException] with an appropriate message
|
||||||
|
*/
|
||||||
fun <E> edgeNotFound(edge: E) =
|
fun <E> edgeNotFound(edge: E) =
|
||||||
GraphException("Edge '$edge' not found in this graph.")
|
GraphException("Edge '$edge' not found in this graph.")
|
||||||
}
|
}
|
||||||
|
51
src/main/kotlin/com/jaytux/altgraph/examples/SimpleCFG.kt
Normal file
51
src/main/kotlin/com/jaytux/altgraph/examples/SimpleCFG.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 javax.swing.JFrame
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
fun getGraph(): IGraph<String, Int> {
|
||||||
|
val graph = BaseGraph<String, Int>()
|
||||||
|
// vertices
|
||||||
|
val entry = graph.addVertex("[ENTRY]", true)
|
||||||
|
val exit = graph.addVertex("[EXIT]")
|
||||||
|
val labels = intArrayOf(198, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210).associateWith { graph.addVertex(".label_$it") }
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
graph.connect(entry, labels[198]!!, count++)
|
||||||
|
arrayOf(
|
||||||
|
198 to 200, 200 to 201, 200 to 202, 201 to 203, 201 to 204, 203 to 200, 204 to 205, 205 to 208, 208 to 209,
|
||||||
|
209 to 200, 209 to 210, 210 to 208, 204 to 206, 206 to 207, 207 to 208
|
||||||
|
).forEach { (from, to) -> graph.connect(labels[from]!!, labels[to]!!, count++) }
|
||||||
|
graph.connect(labels[202]!!, exit, count++)
|
||||||
|
return graph
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPane(graph: IGraph<String, Int>): GraphPane<String, Int> {
|
||||||
|
val pane = GraphPane(graph) { pane, graph ->
|
||||||
|
PseudoForestLayout(graph, 10.0f, 20.0f, 10.0f) { v ->
|
||||||
|
(pane.getComponentFor(v) as DefaultVertexComponent).vertexSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pane
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val graph = getGraph()
|
||||||
|
val pane = getPane(graph)
|
||||||
|
|
||||||
|
// show pane in window
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
val frame = JFrame("Simple Control Flow Graph")
|
||||||
|
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
|
||||||
|
frame.setSize(800, 600)
|
||||||
|
frame.add(pane)
|
||||||
|
frame.pack()
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,29 @@
|
|||||||
package com.jaytux.altgraph.layout
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
interface ISize<S : ISize<S>> {
|
/**
|
||||||
fun width(): Float
|
* A simple 2D size.
|
||||||
fun height(): Float
|
*
|
||||||
fun copy(width: Float, height: Float): S
|
* This class is intentionally kept simple to easily allow wrapping library-specific size classes.
|
||||||
}
|
*
|
||||||
|
* @property width the width
|
||||||
|
* @property height the height
|
||||||
|
*/
|
||||||
|
data class GSize(var width: Float, var height: Float)
|
||||||
|
|
||||||
interface IPoint<P : IPoint<P>> {
|
/**
|
||||||
fun x(): Float
|
* A simple 2D point.
|
||||||
fun y(): Float
|
*
|
||||||
fun copy(x: Float, y: Float): P
|
* This class is intentionally kept simple to easily allow wrapping library-specific point classes.
|
||||||
|
*
|
||||||
|
* @property x the x coordinate
|
||||||
|
* @property y the y coordinate
|
||||||
|
*/
|
||||||
|
data class GPoint(var x: Float, var y: Float) {
|
||||||
|
/**
|
||||||
|
* Adds two points component-wise.
|
||||||
|
*
|
||||||
|
* @param other the other point to add
|
||||||
|
* @return a new point representing the sum of this point and [other]
|
||||||
|
*/
|
||||||
|
operator fun plus(other: GPoint): GPoint = GPoint(x + other.x, y + other.y)
|
||||||
}
|
}
|
@ -2,12 +2,76 @@ package com.jaytux.altgraph.layout
|
|||||||
|
|
||||||
import com.jaytux.altgraph.core.IGraph
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
|
||||||
interface ILayout<V, E, P : IPoint<P>, S : ISize<S>> {
|
/**
|
||||||
|
* A layout algorithm for graphs.
|
||||||
|
*
|
||||||
|
* The layout algorithm is responsible for positioning vertices in 2D space.
|
||||||
|
*
|
||||||
|
* The layout algorithm should cache the results of the (expensive) [compute] operation, such that the [location] and
|
||||||
|
* [boundingBox] operation can be performed efficiently.
|
||||||
|
* However, it is not required to invalidate the cache when [setGraph] or other mutating operations are called.
|
||||||
|
*
|
||||||
|
* @param V the vertex type
|
||||||
|
* @param E the edge type
|
||||||
|
*/
|
||||||
|
interface ILayout<V, E> {
|
||||||
|
/**
|
||||||
|
* Gets the graph to layout.
|
||||||
|
*
|
||||||
|
* @return the graph
|
||||||
|
*/
|
||||||
fun graph(): IGraph<V, E>
|
fun graph(): IGraph<V, E>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the graph to layout.
|
||||||
|
*
|
||||||
|
* @param graph the graph
|
||||||
|
*/
|
||||||
fun setGraph(graph: IGraph<V, E>)
|
fun setGraph(graph: IGraph<V, E>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the layout.
|
||||||
|
*
|
||||||
|
* The computation results should be cached for future calls to [location] and [boundingBox].
|
||||||
|
*/
|
||||||
fun compute()
|
fun compute()
|
||||||
fun location(vertex: V): P
|
|
||||||
fun freezeAt(vertex: V, point: P)
|
/**
|
||||||
|
* Gets the location of a vertex.
|
||||||
|
*
|
||||||
|
* If the layout has not been computed yet, this should invoke [compute].
|
||||||
|
* However, if the layout has been computed, this should return the cached location.
|
||||||
|
*/
|
||||||
|
fun location(vertex: V): GPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freezes a vertex at a specific point.
|
||||||
|
*
|
||||||
|
* Not all layout algorithms support freezing vertices; check the documentation of the specific implementation.
|
||||||
|
* Additionally, the algorithms that support freezing may restrict which vertices can be frozen, or put restrictions
|
||||||
|
* on the position of frozen vertices.
|
||||||
|
*
|
||||||
|
* A frozen vertex will not be moved by subsequent calls to [compute] until it is unfrozen.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex to freeze
|
||||||
|
* @param point the point to freeze the vertex at
|
||||||
|
*/
|
||||||
|
fun freezeAt(vertex: V, point: GPoint)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfreezes a vertex, allowing it to be moved by subsequent calls to [compute].
|
||||||
|
*
|
||||||
|
* If an implementation does not support freezing vertices, this method should do nothing.
|
||||||
|
*
|
||||||
|
* @param vertex the vertex to unfreeze
|
||||||
|
*/
|
||||||
fun unfreeze(vertex: V)
|
fun unfreeze(vertex: V)
|
||||||
fun boundingBox(): S
|
|
||||||
|
/**
|
||||||
|
* Gets the bounding box of the layout.
|
||||||
|
*
|
||||||
|
* If the layout has not been computed yet, this should invoke [compute].
|
||||||
|
* However, if the layout has been computed, this should return the cached bounding box.
|
||||||
|
*/
|
||||||
|
fun boundingBox(): GSize
|
||||||
}
|
}
|
@ -1,3 +0,0 @@
|
|||||||
package com.jaytux.altgraph.layout
|
|
||||||
|
|
||||||
data class MutablePair<T1, T2>(var x: T1, var y: T2)
|
|
@ -8,37 +8,97 @@ import kotlin.concurrent.atomics.AtomicBoolean
|
|||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
/**
|
||||||
|
* A Sugiyama-style layout algorithm for "pseudo-forests" (such as control-flow graphs).
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* All mutation operations are synchronized, and thread-safe. However, the layout is not incremental, and doesn't track
|
||||||
|
* graph changes. Any change to the graph or vertex sizes requires a full recomputation of the layout. The synchronized
|
||||||
|
* operations are:
|
||||||
|
* - Changing the graph ([setGraph]), measuring ([setVertexSize]),
|
||||||
|
* - Querying layout-dependent values ([location], [boundingBox]),
|
||||||
|
* - Computing the layout ([compute]).
|
||||||
|
*
|
||||||
|
* Locking is done via a simple spin-lock.
|
||||||
|
*
|
||||||
|
* @param V the vertex type
|
||||||
|
* @param E the edge type
|
||||||
|
* @param graph the graph to layout
|
||||||
|
* @property horizontalMargin the horizontal margin between vertices in the same layer
|
||||||
|
* @property disjoinXMargin the horizontal margin between disjoint subgraphs
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @see ILayout
|
||||||
|
*/
|
||||||
|
class PseudoForestLayout<V, E>(
|
||||||
graph: IGraph<V, E>,
|
graph: IGraph<V, E>,
|
||||||
var horizontalMargin: Float,
|
var horizontalMargin: Float,
|
||||||
var disjoinXMargin: Float,
|
var disjoinXMargin: Float,
|
||||||
var interLayer: Float,
|
var interLayer: Float,
|
||||||
val pointZero: P, val sizeZero: S,
|
val ignoreInLayout: (E, LayoutPhase) -> Boolean = { _, _ -> false },
|
||||||
vertexSize: (V) -> VertexSize<P, S>
|
vertexSize: (V) -> VertexSize
|
||||||
) : ILayout<V, E, P, S> {
|
) : ILayout<V, E>
|
||||||
data class VertexSize<P : IPoint<P>, S : ISize<S>>(val vertex: S, val labelOffset: P, val labelSize: S) {
|
{
|
||||||
fun fullSize(): S { // TODO: check the math here
|
/**
|
||||||
val minX = 0 + labelOffset.x()
|
* An enum representing the different phases of the layout process where edges can be ignored.
|
||||||
val minY = 0 + labelOffset.y()
|
* - [LAYERING]: during the layering phase, where back-edges are ignored to determine layers.
|
||||||
val maxX = vertex.width() + labelOffset.x() + labelSize.width()
|
* - [DISJOINTS]: during the disjoint graph computation phase, where edges connecting disjoint subgraphs can be ignored.
|
||||||
val maxY = vertex.height() + labelOffset.y() + labelSize.height()
|
* - [SLOT_ASSIGNMENT]: during the slot assignment phase, where certain edges may be ignored to optimize layout.
|
||||||
return vertex.copy(
|
*/
|
||||||
|
enum class LayoutPhase {
|
||||||
|
LAYERING,
|
||||||
|
DISJOINTS,
|
||||||
|
SLOT_ASSIGNMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class representing data on the size of a vertex, including its label offset and size.
|
||||||
|
*
|
||||||
|
* @property vertex the size of the vertex itself
|
||||||
|
* @property labelOffset the offset of the label relative to the vertex's center
|
||||||
|
* @property labelSize the size of the label
|
||||||
|
*/
|
||||||
|
data class VertexSize(val vertex: GSize, val labelOffset: GPoint, val labelSize: GSize) {
|
||||||
|
/**
|
||||||
|
* Calculates the full size of the vertex including its label and offset.
|
||||||
|
*
|
||||||
|
* @return the full size of the vertex
|
||||||
|
*/
|
||||||
|
fun fullSize(): GSize { // TODO: check the math here
|
||||||
|
val minX = 0 + labelOffset.x
|
||||||
|
val minY = 0 + labelOffset.y
|
||||||
|
val maxX = vertex.width + labelOffset.x + labelSize.width
|
||||||
|
val maxY = vertex.height + labelOffset.y + labelSize.height
|
||||||
|
return GSize(
|
||||||
width = maxX - minX,
|
width = maxX - minX,
|
||||||
height = maxY - minY
|
height = maxY - minY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun vCenterInBox(): P { // TODO: check the math here
|
/**
|
||||||
return labelOffset.copy(
|
* Calculates the center point of the vertex within its bounding box.
|
||||||
x = labelOffset.x() + vertex.width() / 2,
|
*
|
||||||
y = labelOffset.y() + vertex.height() / 2
|
* @return the center point of the vertex
|
||||||
|
*/
|
||||||
|
fun vCenterInBox(): GPoint { // TODO: check the math here
|
||||||
|
return GPoint(
|
||||||
|
x = labelOffset.x + vertex.width / 2,
|
||||||
|
y = labelOffset.y + vertex.height / 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var _graph: IGraph<V, E> = graph
|
private var _graph: IGraph<V, E> = graph
|
||||||
private val _positions = mutableMapOf<V, P>()
|
private val _positions = mutableMapOf<V, GPoint>()
|
||||||
private var _vertexSize: (V) -> VertexSize<P, S> = vertexSize
|
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||||
private var _boundingBox: S? = null
|
private var _boundingBox: GSize? = null
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||||
@ -50,93 +110,162 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
println("Took lock in ${Exception().stackTrace.first()}")
|
||||||
val x = block()
|
val x = block()
|
||||||
_lock.store(false) // unlock after operation
|
_lock.store(false) // unlock after operation
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
println("Releasing lock in ${Exception().stackTrace.first()}")
|
||||||
_lock.store(false) // we can safely unlock
|
_lock.store(false) // we can safely unlock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 } }
|
||||||
fun setVertexSize(vertexSize: (V) -> VertexSize<P, S>) { locked { _vertexSize = vertexSize } }
|
|
||||||
|
|
||||||
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
/**
|
||||||
private data class Connector<V>(
|
* Sets the vertex measuring function.
|
||||||
var from: LayeredVertex<V>,
|
*
|
||||||
var to: LayeredVertex<V>
|
* @param vertexSize a function that returns the size of a vertex, including its label offset
|
||||||
)
|
*/
|
||||||
private data class LayeredVertex<V>(
|
fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } }
|
||||||
val x: SumType<V, Connector<V>>
|
|
||||||
) {
|
private class Conn<V> private constructor(var from: Vert<V>?, var to: Vert<V>?, private val _id: Int) {
|
||||||
|
constructor(from: Vert<V>?, to: Vert<V>?) : this(from, to, _nextId++) {}
|
||||||
|
|
||||||
|
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: V): this(x.sum1())
|
||||||
constructor(x: Connector<V>): this(x.sum2())
|
constructor(x: Conn<V>): this(x.sum2())
|
||||||
|
|
||||||
fun same(other: LayeredVertex<V>) = x.fold({ it1 ->
|
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 ->
|
println(" - Checking direct parenthood between $it1 and dummy $it2 (${it2.from}): ${it2.from?.x == it1}")
|
||||||
// x is a vertex
|
it2.from?.x == it1
|
||||||
other.x.fold({
|
|
||||||
// other is a vertex
|
|
||||||
graph.xToY(xx, it) != null
|
|
||||||
}) {
|
|
||||||
// other is a connector
|
|
||||||
it.from.same(this)
|
|
||||||
}
|
}
|
||||||
}) { xx ->
|
}) { it1 ->
|
||||||
// x is a connector -> we need to connect to other
|
it1.to!!.same(other)
|
||||||
xx.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println(" - Breaking edge $from -> $to with chain: $chain")
|
||||||
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
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
println(" - Crossing between ${lTop[conn.first]}->${lBot[conn.second]} and ${lTop[other.first]}->${lBot[other.second]}")
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
override fun compute() {
|
override fun compute() {
|
||||||
|
println("Acquiring lock")
|
||||||
locked {
|
locked {
|
||||||
|
print("Starting layouting")
|
||||||
_positions.clear()
|
_positions.clear()
|
||||||
|
|
||||||
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
||||||
val roots = _graph.roots()
|
val roots = _graph.roots()
|
||||||
if (roots.isEmpty()) { // Only reachable nodes matter.
|
if (roots.isEmpty()) { // Only reachable nodes matter.
|
||||||
_boundingBox = sizeZero
|
_boundingBox = GSize(0.0f, 0.0f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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)) {
|
||||||
|
println(" - Ignoring edge $edge for layout")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (succ in onPath) return@forEach
|
||||||
dep += succ
|
dep += succ
|
||||||
|
|
||||||
if (succ in onPath) return@forEach
|
|
||||||
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
|
||||||
|
|
||||||
val delta = succLayer - l
|
val delta = succLayer - l
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
layers[succ] = succLayer to (sDep)
|
layers[succ] = succLayer to (sDep)
|
||||||
@ -145,27 +274,36 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
?: (succLayer to mutableSetOf())
|
?: (succLayer to mutableSetOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} ?: run {
|
||||||
|
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
|
||||||
|
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() } }
|
||||||
|
|
||||||
// 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 }
|
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
|
||||||
layers.forEach { (vertex, pair) ->
|
layers.forEach { (vertex, pair) ->
|
||||||
val (layer, _) = pair
|
val (layer, _) = pair
|
||||||
val size = vertexSizes[vertex]!!
|
val size = vertexSizes[vertex]!!
|
||||||
layerHeights[layer] = max(layerHeights[layer], size.first.height())
|
layerHeights[layer] = max(layerHeights[layer], size.first.height)
|
||||||
if(layer == 0) {
|
if(layer == 0) {
|
||||||
// Take into account vertex bounding box offset
|
// Take into account vertex bounding box offset
|
||||||
val delta = size.second.y()
|
val delta = size.second.y
|
||||||
if(delta < minOffset) minOffset = delta
|
if(delta < minOffset) minOffset = delta
|
||||||
if(delta > maxOffset) maxOffset = delta
|
if(delta > maxOffset) maxOffset = delta
|
||||||
}
|
}
|
||||||
@ -178,10 +316,16 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
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()
|
||||||
|
|
||||||
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
|
||||||
@ -196,14 +340,18 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}
|
}
|
||||||
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 ->
|
||||||
@ -217,16 +365,31 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val layerWidths = MutableList(layerCount) { -horizontalMargin } // avoid double adding margin on 1st node
|
val layerWidths = MutableList(layerCount) { -horizontalMargin } // avoid double adding margin on 1st node
|
||||||
|
|
||||||
|
// Layout roots
|
||||||
|
layered[0].forEach { root ->
|
||||||
|
root.x.fold({ node ->
|
||||||
|
val w = vertexSizes[node]!!.first.width
|
||||||
|
layerWidths[0] += w + horizontalMargin
|
||||||
|
}) {}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@ -234,7 +397,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
layered[i].forEach { v ->
|
layered[i].forEach { v ->
|
||||||
v.x.fold({ node ->
|
v.x.fold({ node ->
|
||||||
val w = vertexSizes[node]!!.first.width()
|
val w = vertexSizes[node]!!.first.width
|
||||||
layerWidths[i] += w + horizontalMargin
|
layerWidths[i] += w + horizontalMargin
|
||||||
}) { /* do nothing */ }
|
}) { /* do nothing */ }
|
||||||
}
|
}
|
||||||
@ -244,15 +407,40 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
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>>>>()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
// Assign x positions
|
// Assign x positions
|
||||||
layered.forEachIndexed { idx, layer ->
|
layered.forEachIndexed { idx, layer ->
|
||||||
|
println(" - Positioning layer $idx")
|
||||||
var currentX = currentXZero + (maxWidth - layerWidths[idx]) / 2
|
var currentX = currentXZero + (maxWidth - layerWidths[idx]) / 2
|
||||||
layer.forEach { v ->
|
layer.forEach { v ->
|
||||||
v.x.fold({ node ->
|
v.x.fold({ node ->
|
||||||
val offset = vertexSizes[node]!!.second
|
val offset = vertexSizes[node]!!.second
|
||||||
_positions[node] = pointZero.copy(x = currentX + offset.x(), y = layerY[idx] + offset.y())
|
_positions[node] = GPoint(x = currentX + offset.x, y = layerY[idx] + offset.y)
|
||||||
currentX += vertexSizes[node]!!.first.width() + horizontalMargin
|
println(" - Put vertex $node at ${_positions[node]}")
|
||||||
|
currentX += vertexSizes[node]!!.first.width + horizontalMargin
|
||||||
}) { /* do nothing */ }
|
}) { /* do nothing */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,20 +451,22 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
// Compute the bounding box
|
// Compute the bounding box
|
||||||
// min x and y are 0
|
// min x and y are 0
|
||||||
_boundingBox = sizeZero.copy(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
_boundingBox = GSize(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
||||||
|
println("Done layouting")
|
||||||
}
|
}
|
||||||
|
println("Released lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun location(vertex: V): P {
|
override fun location(vertex: V): GPoint {
|
||||||
if (vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
if (vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||||
return _positions[vertex] ?: run {
|
return _positions[vertex] ?: run {
|
||||||
compute()
|
compute()
|
||||||
_positions[vertex]!!
|
_positions[vertex] ?: throw IllegalArgumentException("Vertex $vertex was not layout-ed: $_positions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun freezeAt(vertex: V, point: P) = throw UnsupportedOperationException("PseudoForestLayout does not allow freezing vertices.")
|
override fun freezeAt(vertex: V, point: GPoint) = throw UnsupportedOperationException("PseudoForestLayout does not allow freezing vertices.")
|
||||||
override fun unfreeze(vertex: V) { /* no-op: cannot freeze vertices */ }
|
override fun unfreeze(vertex: V) { /* no-op: cannot freeze vertices */ }
|
||||||
|
|
||||||
override fun boundingBox(): S = _boundingBox ?: run { compute(); _boundingBox!! }
|
override fun boundingBox(): GSize = locked { _boundingBox ?: run { compute(); _boundingBox!! } }
|
||||||
}
|
}
|
@ -1,25 +1,92 @@
|
|||||||
package com.jaytux.altgraph.layout
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sum type (aka tagged union, variant, discriminated union) of two types.
|
||||||
|
*
|
||||||
|
* At any time, a SumType holds either a `T1` or a `T2` value.
|
||||||
|
*
|
||||||
|
* @param T1 the first type
|
||||||
|
* @param T2 the second type
|
||||||
|
*/
|
||||||
sealed class SumType<out T1, out T2> {
|
sealed class SumType<out T1, out T2> {
|
||||||
|
/**
|
||||||
|
* The first alternative of the sum type.
|
||||||
|
*
|
||||||
|
* This class overrides both [equals] and [hashCode] to refer through to the contained value.
|
||||||
|
* Specifically, equality is defined such that only the following hold:
|
||||||
|
* - `SumT1(x) == SumT1(y)` if and only if `x == y`; or
|
||||||
|
* - `SumT1(x) == y` if and only if `x == y`.
|
||||||
|
*
|
||||||
|
* @param T1 the type of the value
|
||||||
|
* @property value the value
|
||||||
|
*/
|
||||||
class SumT1<out T1>(val value: T1) : SumType<T1, Nothing>() {
|
class SumT1<out T1>(val value: T1) : SumType<T1, Nothing>() {
|
||||||
override fun equals(other: Any?): Boolean =
|
override fun equals(other: Any?): Boolean =
|
||||||
other is SumT1<*> && value == other.value
|
(other is SumT1<*> && value == other.value) || value == other
|
||||||
override fun hashCode(): Int = value.hashCode()
|
override fun hashCode(): Int = value.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The second alternative of the sum type.
|
||||||
|
*
|
||||||
|
* This class overrides both [equals] and [hashCode] to refer through to the contained value.
|
||||||
|
* Specifically, equality is defined such that only the following hold:
|
||||||
|
* - `SumT2(x) == SumT2(y)` if and only if `x == y`; or
|
||||||
|
* - `SumT2(x) == y` if and only if `x == y`.
|
||||||
|
*
|
||||||
|
* @param T2 the type of the value
|
||||||
|
* @property value the value
|
||||||
|
*/
|
||||||
class SumT2<out T2>(val value: T2) : SumType<Nothing, T2>() {
|
class SumT2<out T2>(val value: T2) : SumType<Nothing, T2>() {
|
||||||
override fun equals(other: Any?): Boolean =
|
override fun equals(other: Any?): Boolean =
|
||||||
other is SumT2<*> && value == other.value
|
(other is SumT2<*> && value == other.value) || value == other
|
||||||
override fun hashCode(): Int = value.hashCode()
|
override fun hashCode(): Int = value.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern matches on the sum type.
|
||||||
|
*
|
||||||
|
* If the sum type holds a `T1`, invokes [onT1] with the contained value and returns its result.
|
||||||
|
* Otherwise, invokes [onT2] with the contained value and returns its result.
|
||||||
|
*
|
||||||
|
* @param R the return type of the pattern match
|
||||||
|
* @param onT1 the function to invoke if the sum type holds a `T1`
|
||||||
|
* @param onT2 the function to invoke if the sum type holds a `T2`
|
||||||
|
* @return the result of invoking either [onT1] or [onT2]
|
||||||
|
*/
|
||||||
fun <R> fold(onT1: (T1) -> R, onT2: (T2) -> R): R = when(this) {
|
fun <R> fold(onT1: (T1) -> R, onT2: (T2) -> R): R = when(this) {
|
||||||
is SumT1 -> onT1(value)
|
is SumT1 -> onT1(value)
|
||||||
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() }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Constructs a [SumType] holding a `T1`.
|
||||||
|
*
|
||||||
|
* @receiver the value to hold
|
||||||
|
* @return a [SumType] holding the receiver as a `T1`
|
||||||
|
*/
|
||||||
fun <T> T.sum1(): SumType<T, Nothing> = SumT1(this)
|
fun <T> T.sum1(): SumType<T, Nothing> = SumT1(this)
|
||||||
|
/**
|
||||||
|
* Constructs a [SumType] holding a `T2`.
|
||||||
|
*
|
||||||
|
* @receiver the value to hold
|
||||||
|
* @return a [SumType] holding the receiver as a `T2`
|
||||||
|
*/
|
||||||
fun <T> T.sum2(): SumType<Nothing, T> = SumT2(this)
|
fun <T> T.sum2(): SumType<Nothing, T> = SumT2(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
66
src/main/kotlin/com/jaytux/altgraph/swing/DefaultEdge.kt
Normal file
66
src/main/kotlin/com/jaytux/altgraph/swing/DefaultEdge.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import java.awt.Point
|
||||||
|
import java.awt.Polygon
|
||||||
|
import java.awt.geom.QuadCurve2D
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
class QuadraticEdge<E>(
|
||||||
|
var delta: Float = 0.2f,
|
||||||
|
var arrowLen: Int = 10,
|
||||||
|
var color: (E) -> Color = { Color.BLACK },
|
||||||
|
var arrowAngle: Double = Math.PI / 6.0
|
||||||
|
) : IEdgeRenderer<E> {
|
||||||
|
override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) {
|
||||||
|
val g2 = g.create() as Graphics2D
|
||||||
|
g2.color = color(meta)
|
||||||
|
|
||||||
|
val len = from.distance(to).toFloat()
|
||||||
|
val xLen = delta * len
|
||||||
|
|
||||||
|
val fx = from.x.toFloat(); val fy = from.y.toFloat()
|
||||||
|
val tx = to.x.toFloat(); val ty = to.y.toFloat()
|
||||||
|
|
||||||
|
val midX = (fx + tx) / 2
|
||||||
|
val midY = (fy + ty) / 2
|
||||||
|
|
||||||
|
|
||||||
|
val x1 = tx - midX; val y1 = ty - midY
|
||||||
|
val fac = xLen / sqrt(x1 * x1 + y1 * y1)
|
||||||
|
|
||||||
|
val cx = (midX + y1 * fac)
|
||||||
|
val cy = (midY - x1 * fac)
|
||||||
|
|
||||||
|
val dx1 = cx - fx; val dy1 = cy - fy; val d1fac = offsetFrom / sqrt(dx1 * dx1 + dy1 * dy1)
|
||||||
|
val dx2 = tx - cx; val dy2 = ty - cy; val d2fac = offsetTo / sqrt(dx2 * dx2 + dy2 * dy2)
|
||||||
|
|
||||||
|
val x1_ = fx + dx1 * d1fac; val y1_ = fy + dy1 * d1fac
|
||||||
|
val x2_ = tx - dx2 * d2fac; val y2_ = ty - dy2 * d2fac
|
||||||
|
val curve = QuadCurve2D.Float(
|
||||||
|
x1_, y1_,
|
||||||
|
cx, cy,
|
||||||
|
x2_, y2_
|
||||||
|
)
|
||||||
|
g2.draw(curve)
|
||||||
|
|
||||||
|
// draw arrowhead at the end of the curve
|
||||||
|
val angle = atan2(y2_ - cy, x2_ - cx)
|
||||||
|
val arrowX1 = x2_ - arrowLen * cos(angle - arrowAngle).toFloat()
|
||||||
|
val arrowY1 = y2_ - arrowLen * sin(angle - arrowAngle).toFloat()
|
||||||
|
val arrowX2 = x2_ - arrowLen * cos(angle + arrowAngle).toFloat()
|
||||||
|
val arrowY2 = y2_ - arrowLen * sin(angle + arrowAngle).toFloat()
|
||||||
|
val arrowHead = Polygon(
|
||||||
|
intArrayOf(x2_.toInt(), arrowX1.toInt(), arrowX2.toInt()),
|
||||||
|
intArrayOf(y2_.toInt(), arrowY1.toInt(), arrowY2.toInt()),
|
||||||
|
3
|
||||||
|
)
|
||||||
|
g2.fill(arrowHead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <E> defaultEdgeRenderer(): IEdgeRenderer<E> = QuadraticEdge()
|
@ -0,0 +1,87 @@
|
|||||||
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.layout.PseudoForestLayout
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.RenderingHints.KEY_ANTIALIASING
|
||||||
|
import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||||
|
import java.awt.geom.AffineTransform
|
||||||
|
import java.awt.geom.Ellipse2D
|
||||||
|
import javax.swing.JLabel
|
||||||
|
|
||||||
|
class DefaultVertexComponent(
|
||||||
|
label: String,
|
||||||
|
private var _shape: Shape = Ellipse2D.Double(0.0, 0.0, 30.0, 30.0),
|
||||||
|
var labelOffset: Point = Point(20, 7),
|
||||||
|
) : IDrawable {
|
||||||
|
val label = JLabel(label)
|
||||||
|
private lateinit var _preferredSize: Dimension
|
||||||
|
private var _arrowTarget: Point = Point(0, 0)
|
||||||
|
private var _arrowTargetOffset: Float = 0.0f
|
||||||
|
var fillColor: Color = Color.LIGHT_GRAY
|
||||||
|
var borderStroke: Stroke = BasicStroke(1.0f)
|
||||||
|
var borderColor: Color = Color.BLACK
|
||||||
|
|
||||||
|
var shape: Shape
|
||||||
|
get() = _shape
|
||||||
|
set(value) {
|
||||||
|
_shape = value
|
||||||
|
_arrowTarget = Point(value.bounds.width / 2, value.bounds.height / 2)
|
||||||
|
_arrowTargetOffset = (value.bounds.width + value.bounds.height).toFloat() / 4.0f
|
||||||
|
updatePreferredSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
updatePreferredSize()
|
||||||
|
shape = _shape // to initialize arrow target and offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePreferredSize() {
|
||||||
|
val bounds = shape.bounds
|
||||||
|
val textSize = label.preferredSize
|
||||||
|
|
||||||
|
val cx = bounds.width / 2; val cy = bounds.height / 2
|
||||||
|
val tx = cx + labelOffset.x; val ty = cy + labelOffset.y - textSize.height / 2
|
||||||
|
|
||||||
|
val textRect = Rectangle(tx, ty, textSize.width, textSize.height)
|
||||||
|
val total = bounds.union(textRect)
|
||||||
|
_preferredSize = Dimension(total.width, total.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(graphics: Graphics2D) {
|
||||||
|
val g = graphics.create() as Graphics2D
|
||||||
|
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON)
|
||||||
|
val shapeBounds = shape.bounds
|
||||||
|
val cx = shapeBounds.width / 2; val cy = shapeBounds.height / 2
|
||||||
|
val tx = AffineTransform.getTranslateInstance((cx - shapeBounds.centerX), (cy - shapeBounds.centerY))
|
||||||
|
val transShape = tx.createTransformedShape(shape)
|
||||||
|
|
||||||
|
g.color = fillColor
|
||||||
|
g.fill(transShape)
|
||||||
|
g.color = borderColor
|
||||||
|
g.stroke = borderStroke
|
||||||
|
g.draw(transShape)
|
||||||
|
|
||||||
|
val textSize = label.preferredSize
|
||||||
|
val lx = cx + labelOffset.x; val ly = cy + labelOffset.y - textSize.height / 2
|
||||||
|
label.size = textSize
|
||||||
|
g.translate(lx, ly)
|
||||||
|
label.paint(g)
|
||||||
|
|
||||||
|
g.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vertexSize(): PseudoForestLayout.VertexSize {
|
||||||
|
val size = _preferredSize
|
||||||
|
val lSize = label.preferredSize
|
||||||
|
return PseudoForestLayout.VertexSize(size.graph(), labelOffset.graph(), lSize.graph())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asRenderData(): IVertexRenderer.RenderData =
|
||||||
|
IVertexRenderer.RenderData(this, _arrowTarget, _arrowTargetOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V> defaultRenderer(
|
||||||
|
toString: (V) -> String = { it.toString() }
|
||||||
|
) : IVertexRenderer<V> = IVertexRenderer { v: V ->
|
||||||
|
DefaultVertexComponent(toString(v)).asRenderData()
|
||||||
|
}
|
21
src/main/kotlin/com/jaytux/altgraph/swing/Functional.kt
Normal file
21
src/main/kotlin/com/jaytux/altgraph/swing/Functional.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import java.awt.Point
|
||||||
|
|
||||||
|
interface IDrawable {
|
||||||
|
fun draw(graphics: Graphics2D)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface IVertexRenderer<V> {
|
||||||
|
data class RenderData(
|
||||||
|
val drawer: IDrawable,
|
||||||
|
val arrowTarget: Point,
|
||||||
|
val arrowTargetOffset: Float
|
||||||
|
)
|
||||||
|
fun getDrawable(v: V): RenderData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface IEdgeRenderer<E> {
|
||||||
|
fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float)
|
||||||
|
}
|
@ -1,29 +1,12 @@
|
|||||||
package com.jaytux.altgraph.swing
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
import com.jaytux.altgraph.layout.IPoint
|
import com.jaytux.altgraph.layout.GPoint
|
||||||
import com.jaytux.altgraph.layout.ISize
|
import com.jaytux.altgraph.layout.GSize
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
|
|
||||||
class GSize(val size: Dimension) : ISize<GSize> {
|
fun GSize.swing() = Dimension(width.toInt(), height.toInt())
|
||||||
override fun width(): Float = size.width.toFloat()
|
fun GPoint.swing() = Point(x.toInt(), y.toInt())
|
||||||
override fun height(): Float = size.height.toFloat()
|
|
||||||
override fun copy(width: Float, height: Float): GSize =
|
|
||||||
GSize(Dimension(width.toInt(), height.toInt()))
|
|
||||||
|
|
||||||
override fun hashCode(): Int = size.hashCode()
|
fun Dimension.graph() = GSize(width.toFloat(), height.toFloat())
|
||||||
override fun equals(other: Any?): Boolean = other is GSize && size == other.size
|
fun Point.graph() = GPoint(x.toFloat(), y.toFloat())
|
||||||
override fun toString(): String = "$size"
|
|
||||||
}
|
|
||||||
|
|
||||||
class GPoint(val point: Point) : IPoint<GPoint> {
|
|
||||||
override fun x(): Float = point.x.toFloat()
|
|
||||||
override fun y(): Float = point.y.toFloat()
|
|
||||||
override fun copy(x: Float, y: Float): GPoint =
|
|
||||||
GPoint(Point(x.toInt(), y.toInt()))
|
|
||||||
|
|
||||||
override fun hashCode(): Int = point.hashCode()
|
|
||||||
override fun equals(other: Any?): Boolean =
|
|
||||||
other is GPoint && point == other.point
|
|
||||||
override fun toString(): String = "$point"
|
|
||||||
}
|
|
131
src/main/kotlin/com/jaytux/altgraph/swing/GraphPane.kt
Normal file
131
src/main/kotlin/com/jaytux/altgraph/swing/GraphPane.kt
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
import com.jaytux.altgraph.layout.ILayout
|
||||||
|
import java.awt.Graphics
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import java.awt.Point
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.event.MouseMotionListener
|
||||||
|
import java.awt.geom.AffineTransform
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
class GraphPane<V, E>(
|
||||||
|
val graph: IGraph<V, E>,
|
||||||
|
layout: (GraphPane<V, E>, IGraph<V, E>) -> ILayout<V, E>
|
||||||
|
) : JPanel()
|
||||||
|
{
|
||||||
|
private val _delta = AffineTransform()
|
||||||
|
private var _zoom = 1.0
|
||||||
|
private var _panX = 0.0
|
||||||
|
private var _panY = 0.0
|
||||||
|
|
||||||
|
private val _layout: ILayout<V, E>
|
||||||
|
private var _renderer: IVertexRenderer<V> = defaultRenderer()
|
||||||
|
private var _edgeRenderer: IEdgeRenderer<E> = defaultEdgeRenderer()
|
||||||
|
private val _vertices = mutableMapOf<V, IVertexRenderer.RenderData>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.layout = null
|
||||||
|
_delta.setToIdentity()
|
||||||
|
_layout = layout(this, graph)
|
||||||
|
|
||||||
|
addMouseWheelListener { event ->
|
||||||
|
val delta = event.preciseWheelRotation
|
||||||
|
if(delta < 0) {
|
||||||
|
_zoom *= 1.1
|
||||||
|
} else {
|
||||||
|
_zoom /= 1.1
|
||||||
|
}
|
||||||
|
transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
addMouseMotionListener(object : MouseMotionListener {
|
||||||
|
var last: Point? = null
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent?) {
|
||||||
|
e?.let {
|
||||||
|
last?.let {
|
||||||
|
val dx = e.x - it.x
|
||||||
|
val dy = e.y - it.y
|
||||||
|
_panX += dx
|
||||||
|
_panY += dy
|
||||||
|
transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
last = Point(e.x, e.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseMoved(e: MouseEvent?) {
|
||||||
|
e?.let { last = it.point }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transform() {
|
||||||
|
_delta.setToIdentity()
|
||||||
|
_delta.translate(_panX, _panY)
|
||||||
|
_delta.scale(_zoom, _zoom)
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun relayout() {
|
||||||
|
_layout.compute()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getComponentFor(v: V): IDrawable =
|
||||||
|
getRenderDataFor(v).drawer
|
||||||
|
|
||||||
|
fun getRenderDataFor(v: V): IVertexRenderer.RenderData =
|
||||||
|
_vertices.getOrPut(v) { _renderer.getDrawable(v) }
|
||||||
|
|
||||||
|
fun setRenderer(renderer: IVertexRenderer<V>) {
|
||||||
|
_renderer = renderer
|
||||||
|
_vertices.clear()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEdgeRenderer(renderer: IEdgeRenderer<E>) {
|
||||||
|
_edgeRenderer = renderer
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetComponents() {
|
||||||
|
_vertices.clear()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paintComponent(g: Graphics?) {
|
||||||
|
super.paintComponent(g)
|
||||||
|
if(g == null) return
|
||||||
|
|
||||||
|
val g2 = g.create() as Graphics2D
|
||||||
|
g2.transform = _delta
|
||||||
|
graph.vertices().forEach { v -> getRenderDataFor(v) } // ensure all vertices are rendered
|
||||||
|
_vertices.forEach { (v, c) ->
|
||||||
|
if(v !in graph.vertices()) return@forEach
|
||||||
|
val pos = _layout.location(v).swing()
|
||||||
|
|
||||||
|
val g3 = g2.create() as Graphics2D
|
||||||
|
g3.translate(pos.x, pos.y)
|
||||||
|
c.drawer.draw(g3)
|
||||||
|
g3.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.edges().forEach { (e, vv) ->
|
||||||
|
val first = getRenderDataFor(vv.first)
|
||||||
|
val second = getRenderDataFor(vv.second)
|
||||||
|
val from = _layout.location(vv.first) + first.arrowTarget.graph()
|
||||||
|
val to = _layout.location(vv.second) + second.arrowTarget.graph()
|
||||||
|
|
||||||
|
_edgeRenderer.drawEdge(g2,
|
||||||
|
from.swing(),
|
||||||
|
to.swing(),
|
||||||
|
e, first.arrowTargetOffset, second.arrowTargetOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
g2.dispose()
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user