Compare commits
3 Commits
99d64242cf
...
d460c71a33
Author | SHA1 | Date | |
---|---|---|---|
d460c71a33
|
|||
4a5db84148
|
|||
64560b172b
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
kotlin("jvm") version "2.2.0"
|
kotlin("jvm") version "2.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "be.topl.phoenix-intellij"
|
group = "com.jaytux.altgraph"
|
||||||
version = "1.0-SNAPSHOT"
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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,83 @@ 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 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,
|
vertexSize: (V) -> VertexSize
|
||||||
vertexSize: (V) -> VertexSize<P, S>
|
) : ILayout<V, E>
|
||||||
) : ILayout<V, E, P, S> {
|
{
|
||||||
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
|
* A class representing data on the size of a vertex, including its label offset and size.
|
||||||
val minX = 0 + labelOffset.x()
|
*
|
||||||
val minY = 0 + labelOffset.y()
|
* @property vertex the size of the vertex itself
|
||||||
val maxX = vertex.width() + labelOffset.x() + labelSize.width()
|
* @property labelOffset the offset of the label relative to the vertex's center
|
||||||
val maxY = vertex.height() + labelOffset.y() + labelSize.height()
|
* @property labelSize the size of the label
|
||||||
return vertex.copy(
|
*/
|
||||||
|
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,18 +96,26 @@ 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 } }
|
|
||||||
|
/**
|
||||||
|
* Sets the vertex measuring function.
|
||||||
|
*
|
||||||
|
* @param vertexSize a function that returns the size of a vertex, including its label offset
|
||||||
|
*/
|
||||||
|
fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } }
|
||||||
|
|
||||||
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
||||||
private data class Connector<V>(
|
private data class Connector<V>(
|
||||||
@ -114,29 +168,36 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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, _) ->
|
||||||
|
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 +206,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,9 +248,17 @@ 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 = layers[root]?.second?.toMutableSet() ?: return@fold acc
|
||||||
|
reachable += root
|
||||||
|
|
||||||
val dedup = acc.mapNotNull { other ->
|
val dedup = acc.mapNotNull { other ->
|
||||||
val inter = reachable intersect other
|
val inter = reachable intersect other
|
||||||
@ -196,6 +274,7 @@ 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 {
|
||||||
@ -204,6 +283,9 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}.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 ->
|
||||||
@ -223,6 +305,15 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -234,7 +325,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 */ }
|
||||||
}
|
}
|
||||||
@ -247,12 +338,14 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
// 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 +356,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,80 @@
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
|
||||||
|
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