Documentation
This commit is contained in:
@ -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.")
|
||||
}
|
||||
|
@ -1,4 +1,21 @@
|
||||
package com.jaytux.altgraph.layout
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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)
|
@ -2,12 +2,76 @@ package com.jaytux.altgraph.layout
|
||||
|
||||
import com.jaytux.altgraph.core.IGraph
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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,14 +8,56 @@ import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* 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,
|
||||
vertexSize: (V) -> VertexSize
|
||||
) : ILayout<V, E> {
|
||||
) : 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
|
||||
@ -27,6 +69,11 @@ class PseudoForestLayout<V, E>(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@ -60,6 +107,12 @@ class PseudoForestLayout<V, E>(
|
||||
|
||||
override fun graph(): IGraph<V, E> = _graph
|
||||
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -268,14 +321,16 @@ class PseudoForestLayout<V, E>(
|
||||
|
||||
override fun location(vertex: V): GPoint {
|
||||
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||
return _positions[vertex] ?: run {
|
||||
compute()
|
||||
_positions[vertex]!!
|
||||
return locked {
|
||||
_positions[vertex] ?: run {
|
||||
compute()
|
||||
_positions[vertex]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(): GSize = _boundingBox ?: run { compute(); _boundingBox!! }
|
||||
override fun boundingBox(): GSize = locked { _boundingBox ?: run { compute(); _boundingBox!! } }
|
||||
}
|
@ -1,25 +1,78 @@
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user