From 4a5db84148bb46222245efccf0fc442f9ecbd106 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Fri, 22 Aug 2025 21:21:18 +0200 Subject: [PATCH] Documentation --- .../com/jaytux/altgraph/core/BaseGraph.kt | 23 +++ .../kotlin/com/jaytux/altgraph/core/IGraph.kt | 179 ++++++++++++++++++ .../com/jaytux/altgraph/layout/Geometry.kt | 17 ++ .../com/jaytux/altgraph/layout/ILayout.kt | 64 +++++++ .../com/jaytux/altgraph/layout/MutablePair.kt | 3 - .../altgraph/layout/PseudoForestLayout.kt | 65 ++++++- .../com/jaytux/altgraph/layout/SumType.kt | 57 +++++- 7 files changed, 398 insertions(+), 10 deletions(-) delete mode 100644 src/main/kotlin/com/jaytux/altgraph/layout/MutablePair.kt diff --git a/src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt b/src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt index b8b69db..f0295f5 100644 --- a/src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt +++ b/src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt @@ -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 : IMutableGraph { private val _vertices = mutableMapOf() // [from] -> {e: exists v s.t. e = (from, v)} @@ -70,6 +84,15 @@ open class BaseGraph : IMutableGraph { 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 = BaseGraphImmutable() override fun successors(vertex: V): Map = diff --git a/src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt b/src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt index ccd4541..84a30b4 100644 --- a/src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt +++ b/src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt @@ -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 { + /** + * Gets all the vertices in the graph. + * + * @return a set of all vertices in the graph + */ fun vertices(): Set + + /** + * 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> + + /** + * Gets all the root vertices in the graph. + * + * @return a set of all root vertices in the graph + */ fun roots(): Set + /** + * 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 = 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 = 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 : IGraph { + /** + * 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 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 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 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 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 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 edgeNotFound(edge: E) = GraphException("Edge '$edge' not found in this graph.") } diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt b/src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt index 091e764..f071b12 100644 --- a/src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt +++ b/src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt @@ -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) \ No newline at end of file diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt b/src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt index 0254ec9..8abec95 100644 --- a/src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt +++ b/src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt @@ -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 { + /** + * Gets the graph to layout. + * + * @return the graph + */ fun graph(): IGraph + + /** + * Sets the graph to layout. + * + * @param graph the graph + */ fun setGraph(graph: IGraph) + + /** + * 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 } \ No newline at end of file diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/MutablePair.kt b/src/main/kotlin/com/jaytux/altgraph/layout/MutablePair.kt deleted file mode 100644 index 12ddc05..0000000 --- a/src/main/kotlin/com/jaytux/altgraph/layout/MutablePair.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.jaytux.altgraph.layout - -data class MutablePair(var x: T1, var y: T2) \ No newline at end of file diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt b/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt index b36a90a..12a7211 100644 --- a/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt +++ b/src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt @@ -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( graph: IGraph, var horizontalMargin: Float, var disjoinXMargin: Float, var interLayer: Float, vertexSize: (V) -> VertexSize -) : ILayout { +) : ILayout +{ + /** + * 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( ) } + /** + * 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( override fun graph(): IGraph = _graph override fun setGraph(graph: IGraph) { 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( 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!! } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt b/src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt index e501e11..ad1f44b 100644 --- a/src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt +++ b/src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt @@ -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 { + /** + * 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(val value: T1) : SumType() { 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(val value: T2) : SumType() { 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 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.sum1(): SumType = SumT1(this) + /** + * Constructs a [SumType] holding a `T2`. + * + * @receiver the value to hold + * @return a [SumType] holding the receiver as a `T2` + */ fun T.sum2(): SumType = SumT2(this) } } \ No newline at end of file