Compare commits
3 Commits
99d64242cf
...
d460c71a33
Author | SHA1 | Date | |
---|---|---|---|
d460c71a33
|
|||
4a5db84148
|
|||
64560b172b
|
@ -2,7 +2,7 @@ plugins {
|
||||
kotlin("jvm") version "2.2.0"
|
||||
}
|
||||
|
||||
group = "be.topl.phoenix-intellij"
|
||||
group = "com.jaytux.altgraph"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
|
@ -1,5 +1,19 @@
|
||||
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> {
|
||||
private val _vertices = mutableMapOf<V, Boolean>()
|
||||
// [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) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
override fun successors(vertex: V): Map<V, E> =
|
||||
|
@ -1,42 +1,221 @@
|
||||
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> {
|
||||
/**
|
||||
* Gets all the vertices in the graph.
|
||||
*
|
||||
* @return a set of all vertices in the graph
|
||||
*/
|
||||
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>>
|
||||
|
||||
/**
|
||||
* Gets all the root vertices in the graph.
|
||||
*
|
||||
* @return a set of all root vertices in the graph
|
||||
*/
|
||||
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> =
|
||||
edges().filter { it.value.first == vertex }
|
||||
.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> =
|
||||
edges().filter { it.value.second == vertex }
|
||||
.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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when a graph operation fails.
|
||||
*
|
||||
* @param message the exception message
|
||||
*/
|
||||
class GraphException(message: String) : RuntimeException(message) {
|
||||
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) =
|
||||
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) =
|
||||
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) =
|
||||
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) =
|
||||
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) =
|
||||
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) =
|
||||
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
|
||||
|
||||
interface ISize<S : ISize<S>> {
|
||||
fun width(): Float
|
||||
fun height(): Float
|
||||
fun copy(width: Float, height: Float): S
|
||||
}
|
||||
/**
|
||||
* A simple 2D size.
|
||||
*
|
||||
* 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
|
||||
fun y(): Float
|
||||
fun copy(x: Float, y: Float): P
|
||||
/**
|
||||
* A simple 2D point.
|
||||
*
|
||||
* 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
|
||||
|
||||
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>
|
||||
|
||||
/**
|
||||
* Sets the graph to layout.
|
||||
*
|
||||
* @param graph the graph
|
||||
*/
|
||||
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 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 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.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>,
|
||||
var horizontalMargin: Float,
|
||||
var disjoinXMargin: Float,
|
||||
var interLayer: Float,
|
||||
val pointZero: P, val sizeZero: S,
|
||||
vertexSize: (V) -> VertexSize<P, S>
|
||||
) : 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
|
||||
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 vertex.copy(
|
||||
vertexSize: (V) -> VertexSize
|
||||
) : ILayout<V, E>
|
||||
{
|
||||
/**
|
||||
* 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,
|
||||
height = maxY - minY
|
||||
)
|
||||
}
|
||||
|
||||
fun vCenterInBox(): P { // TODO: check the math here
|
||||
return labelOffset.copy(
|
||||
x = labelOffset.x() + vertex.width() / 2,
|
||||
y = labelOffset.y() + vertex.height() / 2
|
||||
/**
|
||||
* Calculates the center point of the vertex within its bounding box.
|
||||
*
|
||||
* @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 val _positions = mutableMapOf<V, P>()
|
||||
private var _vertexSize: (V) -> VertexSize<P, S> = vertexSize
|
||||
private var _boundingBox: S? = null
|
||||
private val _positions = mutableMapOf<V, GPoint>()
|
||||
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||
private var _boundingBox: GSize? = null
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||
@ -50,18 +96,26 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
}
|
||||
|
||||
try {
|
||||
println("Took lock in ${Exception().stackTrace.first()}")
|
||||
val x = block()
|
||||
_lock.store(false) // unlock after operation
|
||||
return x
|
||||
}
|
||||
finally {
|
||||
println("Releasing lock in ${Exception().stackTrace.first()}")
|
||||
_lock.store(false) // we can safely unlock
|
||||
}
|
||||
}
|
||||
|
||||
override fun graph(): IGraph<V, E> = _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
|
||||
private data class Connector<V>(
|
||||
@ -114,29 +168,36 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
}
|
||||
|
||||
override fun compute() {
|
||||
println("Acquiring lock")
|
||||
locked {
|
||||
print("Starting layouting")
|
||||
_positions.clear()
|
||||
|
||||
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
||||
val roots = _graph.roots()
|
||||
if (roots.isEmpty()) { // Only reachable nodes matter.
|
||||
_boundingBox = sizeZero
|
||||
_boundingBox = GSize(0.0f, 0.0f)
|
||||
return
|
||||
}
|
||||
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
||||
queue.addAll(roots.map { it to emptySet() })
|
||||
println(" - Computing layers from roots: $roots")
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
val (vertex, onPath) = queue.removeFirst()
|
||||
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
|
||||
|
||||
val succLayer = layer + 1
|
||||
_graph.successors(vertex).forEach { (succ, _) ->
|
||||
if (succ in onPath) return@forEach
|
||||
dep += succ
|
||||
|
||||
if (succ in onPath) return@forEach
|
||||
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
|
||||
if (delta > 0) {
|
||||
layers[succ] = succLayer to (sDep)
|
||||
@ -145,27 +206,36 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
?: (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)
|
||||
}
|
||||
|
||||
// ensure dependents are always up to date
|
||||
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
|
||||
}
|
||||
println(" - Assigned layers:")
|
||||
layers.forEach { (v, p) -> println(" - Vertex $v: layer ${p.first}, dependents: ${p.second}") }
|
||||
|
||||
// Cache node sizes
|
||||
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
||||
|
||||
// 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 }
|
||||
println(" - Have $layerCount layers")
|
||||
|
||||
var minOffset = Float.POSITIVE_INFINITY
|
||||
var maxOffset = Float.NEGATIVE_INFINITY
|
||||
layers.forEach { (vertex, pair) ->
|
||||
val (layer, _) = pair
|
||||
val size = vertexSizes[vertex]!!
|
||||
layerHeights[layer] = max(layerHeights[layer], size.first.height())
|
||||
layerHeights[layer] = max(layerHeights[layer], size.first.height)
|
||||
if(layer == 0) {
|
||||
// Take into account vertex bounding box offset
|
||||
val delta = size.second.y()
|
||||
val delta = size.second.y
|
||||
if(delta < minOffset) minOffset = delta
|
||||
if(delta > maxOffset) maxOffset = delta
|
||||
}
|
||||
@ -178,9 +248,17 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
y
|
||||
}
|
||||
|
||||
println(" - Layer measurements: (height, y) = ${(layerHeights zip layerY)}")
|
||||
println(" - Layers with nodes:")
|
||||
for(i in 0 until layerCount) {
|
||||
val verts = layers.filter { it.value.first == i }.keys
|
||||
println(" - Layer $i: $verts")
|
||||
}
|
||||
|
||||
// Compute disjoint graphs
|
||||
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||
val reachable = layers[root]?.second?.toMutableSet() ?: return@fold acc
|
||||
reachable += root
|
||||
|
||||
val dedup = acc.mapNotNull { other ->
|
||||
val inter = reachable intersect other
|
||||
@ -196,6 +274,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
}
|
||||
var currentXZero = 0.0f
|
||||
disjoint.forEach { sub ->
|
||||
println(" - Layouting disjoint subgraph: $sub")
|
||||
// Put each vertex in a list by layer
|
||||
val layered = List(layerCount) { layer ->
|
||||
sub.mapNotNull {
|
||||
@ -204,6 +283,9 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
println(" - Initial layered vertices:")
|
||||
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
|
||||
|
||||
// Break up multi-layer edges with dummy nodes
|
||||
layered.forEachIndexed { idx, list ->
|
||||
list.forEach { v ->
|
||||
@ -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
|
||||
|
||||
// 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)
|
||||
for(i in 1 until layered.size) {
|
||||
// Barycenter heuristic: average of parents' slots
|
||||
@ -234,7 +325,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
|
||||
layered[i].forEach { v ->
|
||||
v.x.fold({ node ->
|
||||
val w = vertexSizes[node]!!.first.width()
|
||||
val w = vertexSizes[node]!!.first.width
|
||||
layerWidths[i] += w + horizontalMargin
|
||||
}) { /* do nothing */ }
|
||||
}
|
||||
@ -247,12 +338,14 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
|
||||
// Assign x positions
|
||||
layered.forEachIndexed { idx, layer ->
|
||||
println(" - Positioning layer $idx")
|
||||
var currentX = currentXZero + (maxWidth - layerWidths[idx]) / 2
|
||||
layer.forEach { v ->
|
||||
v.x.fold({ node ->
|
||||
val offset = vertexSizes[node]!!.second
|
||||
_positions[node] = pointZero.copy(x = currentX + offset.x(), y = layerY[idx] + offset.y())
|
||||
currentX += vertexSizes[node]!!.first.width() + horizontalMargin
|
||||
_positions[node] = GPoint(x = currentX + offset.x, y = layerY[idx] + offset.y)
|
||||
println(" - Put vertex $node at ${_positions[node]}")
|
||||
currentX += vertexSizes[node]!!.first.width + horizontalMargin
|
||||
}) { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
@ -263,20 +356,22 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||
|
||||
// Compute the bounding box
|
||||
// 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 {
|
||||
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||
override fun location(vertex: V): GPoint {
|
||||
if (vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||
return _positions[vertex] ?: run {
|
||||
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 boundingBox(): S = _boundingBox ?: run { compute(); _boundingBox!! }
|
||||
override fun boundingBox(): GSize = locked { _boundingBox ?: run { compute(); _boundingBox!! } }
|
||||
}
|
@ -1,25 +1,80 @@
|
||||
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> {
|
||||
/**
|
||||
* 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>() {
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>() {
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
is SumT1 -> onT1(value)
|
||||
is SumT2 -> onT2(value)
|
||||
}
|
||||
|
||||
override fun toString(): String = fold({ it.toString() }) { it.toString() }
|
||||
|
||||
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)
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
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
|
||||
|
||||
import com.jaytux.altgraph.layout.IPoint
|
||||
import com.jaytux.altgraph.layout.ISize
|
||||
import com.jaytux.altgraph.layout.GPoint
|
||||
import com.jaytux.altgraph.layout.GSize
|
||||
import java.awt.Dimension
|
||||
import java.awt.Point
|
||||
|
||||
class GSize(val size: Dimension) : ISize<GSize> {
|
||||
override fun width(): Float = size.width.toFloat()
|
||||
override fun height(): Float = size.height.toFloat()
|
||||
override fun copy(width: Float, height: Float): GSize =
|
||||
GSize(Dimension(width.toInt(), height.toInt()))
|
||||
fun GSize.swing() = Dimension(width.toInt(), height.toInt())
|
||||
fun GPoint.swing() = Point(x.toInt(), y.toInt())
|
||||
|
||||
override fun hashCode(): Int = size.hashCode()
|
||||
override fun equals(other: Any?): Boolean = other is GSize && size == other.size
|
||||
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"
|
||||
}
|
||||
fun Dimension.graph() = GSize(width.toFloat(), height.toFloat())
|
||||
fun Point.graph() = GPoint(x.toFloat(), y.toFloat())
|
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