[geos-commits] [SCM] GEOS branch main updated. be17b189a8accbf68d5f76f8b45237fd2307011c
git at osgeo.org
git at osgeo.org
Tue Mar 31 15:27:05 PDT 2026
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "GEOS".
The branch, main has been updated
via be17b189a8accbf68d5f76f8b45237fd2307011c (commit)
via 224d1448e5086c2f0017c4c58b05a25823d27173 (commit)
from 8b2ee5e04ea2f3d9b088505cb12d270fa805c51f (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit be17b189a8accbf68d5f76f8b45237fd2307011c
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Tue Mar 31 15:23:11 2026 -0700
Add
extern size_t GEOS_DLL *GEOSMinimumSpanningTree(const GEOSGeometry * const geoms[], unsigned int ngeoms);
for an input set of LineString, build a connected graph using start/end points
as nodes, and calculate the Minimum Spanning Tree for the graph, returning
for each input LineString a non-zero number of the edge is in the
MST and zero otherwise. Different non-zero numbers correspond to member
ship in different MST of unconnected regions of the graph.
diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index 54d29bba9..1341f6365 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -1123,6 +1123,12 @@ extern "C" {
return GEOSLineMerge_r(handle, g);
}
+ std::size_t*
+ GEOSMinimumSpanningTree(const Geometry* const* geoms, unsigned int ngeoms)
+ {
+ return GEOSMinimumSpanningTree_r(handle, geoms, ngeoms);
+ }
+
Geometry*
GEOSLineMergeDirected(const Geometry* g)
{
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index b38029835..2c21576fc 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -1229,6 +1229,12 @@ extern GEOSGeometry GEOS_DLL *GEOSLineMerge_r(
GEOSContextHandle_t handle,
const GEOSGeometry* g);
+/** \see GEOSMinimumSpanningTree */
+extern size_t GEOS_DLL *GEOSMinimumSpanningTree_r(
+ GEOSContextHandle_t handle,
+ const GEOSGeometry * const geoms[],
+ unsigned int ngeoms);
+
/** \see GEOSLineMergeDirected */
extern GEOSGeometry GEOS_DLL *GEOSLineMergeDirected_r(
GEOSContextHandle_t handle,
@@ -5413,8 +5419,25 @@ extern GEOSGeometry GEOS_DLL *GEOSDensify(
extern GEOSGeometry GEOS_DLL *GEOSLineMerge(const GEOSGeometry* g);
/**
-* Merges a set of LineStrings,
-* joining them at nodes which have cardinality 2.
+ * Computes the Minimum Spanning Tree of the given Curves.
+ *
+ * The input is an array of Curves (LineString, CircularString, CompoundCurve)
+ * which form the edges of a graph.
+ * The nodes of the graph are the endpoints of the Curves.
+ * Coincident endpoints are treated as the same node.
+ *
+ * \param geoms Input array of Curves. Non-Curve geometries are ignored.
+ * \param ngeoms Number of geometries in the input array.
+ * \return An array of size_t of size ngeoms. 0 if the edge is not in the MST.
+ * Values > 0 indicate the component ID of the subtree the edge belongs to.
+ * The caller is responsible for freeing the returned array using GEOSFree().
+ * Returns NULL if ngeoms is 0 or on error.
+ * \since 3.15.0
+ */
+extern size_t GEOS_DLL *GEOSMinimumSpanningTree(const GEOSGeometry * const geoms[], unsigned int ngeoms);
+
+/**
+ * Merges a set of LineStrings,* joining them at nodes which have cardinality 2.
* and where the lines have the same direction.
* This means that lines do not have their direction reversed.
*
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 05c83acdf..d183be9ea 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -85,6 +85,7 @@
#include <geos/operation/grid/Grid.h>
#include <geos/operation/grid/GridIntersection.h>
#include <geos/operation/linemerge/LineMerger.h>
+#include <geos/operation/spanning/SpanningTree.h>
#include <geos/operation/intersection/Rectangle.h>
#include <geos/operation/intersection/RectangleIntersection.h>
#include <geos/operation/overlay/snap/GeometrySnapper.h>
@@ -2705,6 +2706,46 @@ extern "C" {
});
}
+ std::size_t*
+ GEOSMinimumSpanningTree_r(GEOSContextHandle_t extHandle, const Geometry* const* geoms, unsigned int ngeoms)
+ {
+ using geos::operation::spanning::SpanningTree;
+ using geos::geom::Curve;
+
+ if (ngeoms == 0 || geoms == nullptr) {
+ return nullptr;
+ }
+
+ return execute(extHandle, [&]() {
+ std::vector<const Curve*> curvevec(ngeoms);
+ for (unsigned int i = 0; i < ngeoms; ++i) {
+ if (geoms[i]) {
+ auto typeId = geoms[i]->getGeometryTypeId();
+ if (typeId == geos::geom::GEOS_LINESTRING ||
+ typeId == geos::geom::GEOS_CIRCULARSTRING ||
+ typeId == geos::geom::GEOS_COMPOUNDCURVE) {
+ curvevec[i] = static_cast<const Curve*>(geoms[i]);
+ }
+ else {
+ curvevec[i] = nullptr;
+ }
+ }
+ else {
+ curvevec[i] = nullptr;
+ }
+ }
+
+ std::vector<std::size_t> result;
+ SpanningTree::mst(curvevec, result);
+
+ std::size_t* result_buf = static_cast<std::size_t*>(malloc(ngeoms * sizeof(std::size_t)));
+ if (result_buf) {
+ std::copy(result.begin(), result.end(), result_buf);
+ }
+ return result_buf;
+ });
+ }
+
Geometry*
GEOSLineMergeDirected_r(GEOSContextHandle_t extHandle, const Geometry* g)
{
diff --git a/include/geos/operation/spanning/SpanningTree.h b/include/geos/operation/spanning/SpanningTree.h
new file mode 100644
index 000000000..9c35cac37
--- /dev/null
+++ b/include/geos/operation/spanning/SpanningTree.h
@@ -0,0 +1,59 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 Paul Ramsey
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Curve;
+}
+}
+
+namespace geos {
+namespace operation { // geos::operation
+namespace spanning { // geos::operation::spanning
+
+/** \brief
+ * Constructs a Minimum Spanning Tree (MST) from a set of Curves.
+ *
+ * The input is a vector of Curves (LineString, CircularString, CompoundCurve)
+ * which form the edges of a graph.
+ * The nodes of the graph are the endpoints of the Curves.
+ * Coincident endpoints are treated as the same node.
+ *
+ * The algorithm uses Kruskal's algorithm to find the MST.
+ */
+class GEOS_DLL SpanningTree {
+
+public:
+
+ /** \brief
+ * Computes the Minimum Spanning Tree of the given Curves.
+ *
+ * @param curves Input vector of Curves.
+ * @param result Output vector of size_t. 0 if the edge is not in the MST.
+ * Values > 0 indicate the component ID of the subtree the edge belongs to.
+ * The vector will be resized to match the input size.
+ */
+ static void mst(const std::vector<const geom::Curve*>& curves, std::vector<std::size_t>& result);
+
+};
+
+} // namespace geos::operation::spanning
+} // namespace geos::operation
+} // namespace geos
diff --git a/src/operation/spanning/SpanningTree.cpp b/src/operation/spanning/SpanningTree.cpp
new file mode 100644
index 000000000..41c12abf6
--- /dev/null
+++ b/src/operation/spanning/SpanningTree.cpp
@@ -0,0 +1,159 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 Paul Ramsey
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/operation/spanning/SpanningTree.h>
+#include <geos/geom/Curve.h>
+#include <geos/geom/Point.h>
+#include <geos/geom/Coordinate.h>
+
+#include <vector>
+#include <algorithm>
+#include <unordered_map>
+#include <numeric>
+
+using namespace geos::geom;
+
+namespace geos {
+namespace operation { // geos::operation
+namespace spanning { // geos::operation::spanning
+
+namespace {
+
+ // Internal edge representation for Kruskal's
+ struct MSTEdge {
+ std::size_t u;
+ std::size_t v;
+ double weight;
+ std::size_t originalIndex;
+ };
+
+ // Union-Find data structure using vector for efficiency
+ struct UnionFind {
+ std::vector<std::size_t> parent;
+ std::vector<int> rank;
+
+ UnionFind(std::size_t n) : parent(n), rank(n, 0) {
+ std::iota(parent.begin(), parent.end(), 0);
+ }
+
+ std::size_t find(std::size_t i) {
+ if (parent[i] == i)
+ return i;
+ else
+ parent[i] = find(parent[i]);
+ return parent[i]; // path compression
+ }
+
+ bool unite(std::size_t i, std::size_t j) {
+ std::size_t root_i = find(i);
+ std::size_t root_j = find(j);
+ if (root_i != root_j) {
+ // Union by rank
+ if (rank[root_i] < rank[root_j])
+ parent[root_i] = root_j;
+ else if (rank[root_i] > rank[root_j])
+ parent[root_j] = root_i;
+ else {
+ parent[root_i] = root_j;
+ rank[root_j]++;
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
+ // Helper for coordinate to node ID mapping
+ struct NodeMapping {
+ std::unordered_map<CoordinateXY, std::size_t, CoordinateXY::HashCode> coordToId;
+
+ std::size_t getId(const CoordinateXY& c) {
+ auto it = coordToId.find(c);
+ if (it != coordToId.end()) return it->second;
+ std::size_t id = coordToId.size();
+ coordToId[c] = id;
+ return id;
+ }
+
+ std::size_t size() const {
+ return coordToId.size();
+ }
+ };
+
+} // anonymous namespace
+
+void
+SpanningTree::mst(const std::vector<const geom::Curve*>& curves, std::vector<std::size_t>& result)
+{
+ // Resize result to match input and initialize with 0
+ result.assign(curves.size(), 0);
+ if (curves.empty()) return;
+
+ NodeMapping mapping;
+ std::vector<MSTEdge> edges;
+
+ for (std::size_t i = 0; i < curves.size(); ++i) {
+ const Curve* curve = curves[i];
+ if (!curve || curve->isEmpty()) continue;
+
+ auto startPoint = curve->getStartPoint();
+ auto endPoint = curve->getEndPoint();
+ if (!startPoint || !endPoint) continue;
+
+ CoordinateXY startCoord(*(startPoint->getCoordinate()));
+ CoordinateXY endCoord(*(endPoint->getCoordinate()));
+
+ // Ignore zero-length edges (where start == end in 2D)
+ if (startCoord.equals2D(endCoord)) continue;
+
+ std::size_t u = mapping.getId(startCoord);
+ std::size_t v = mapping.getId(endCoord);
+ double length = curve->getLength();
+
+ edges.push_back({u, v, length, i});
+ }
+
+ if (edges.empty()) return;
+
+ // Sort edges by length
+ std::sort(edges.begin(), edges.end(), [](const MSTEdge& a, const MSTEdge& b) {
+ return a.weight < b.weight;
+ });
+
+ // Kruskal's algorithm
+ UnionFind uf(mapping.size());
+ std::vector<const MSTEdge*> treeEdges;
+
+ for (const auto& edge : edges) {
+ if (uf.unite(edge.u, edge.v)) {
+ treeEdges.push_back(&edge);
+ }
+ }
+
+ // Assign component IDs
+ std::size_t componentId = 0;
+ std::unordered_map<std::size_t, std::size_t> rootToComponentId;
+
+ for (const auto* edge : treeEdges) {
+ std::size_t root = uf.find(edge->u);
+ if (rootToComponentId.find(root) == rootToComponentId.end()) {
+ rootToComponentId[root] = ++componentId;
+ }
+ result[edge->originalIndex] = rootToComponentId[root];
+ }
+}
+
+} // namespace geos::operation::spanning
+} // namespace geos::operation
+} // namespace geos
diff --git a/tests/unit/capi/GEOSMinimumSpanningTreeTest.cpp b/tests/unit/capi/GEOSMinimumSpanningTreeTest.cpp
new file mode 100644
index 000000000..ce9b3f3cb
--- /dev/null
+++ b/tests/unit/capi/GEOSMinimumSpanningTreeTest.cpp
@@ -0,0 +1,175 @@
+//
+// Test Suite for C-API GEOSMinimumSpanningTree
+
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+
+#include <vector>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used in test cases.
+struct test_capigeosmst_data : public capitest::utility {
+};
+
+typedef test_group<test_capigeosmst_data> group;
+typedef group::object object;
+
+group test_capigeosmst_group("capi::GEOSMinimumSpanningTree");
+
+//
+// Test Cases
+//
+
+// Standard case
+template<>
+template<>
+void object::test<1> ()
+{
+ constexpr int size = 3;
+ GEOSGeometry* geoms[size];
+ geoms[0] = GEOSGeomFromWKT("LINESTRING(0 0, 10 0)"); // 10
+ geoms[1] = GEOSGeomFromWKT("LINESTRING(10 0, 5 10)"); // ~11.18
+ geoms[2] = GEOSGeomFromWKT("LINESTRING(5 10, 0 0)"); // ~11.18
+
+ size_t* result = GEOSMinimumSpanningTree(geoms, size);
+
+ ensure(nullptr != result);
+
+ int count = 0;
+ for (int i = 0; i < size; ++i) {
+ if (result[i] > 0) count++;
+ }
+ ensure_equals(count, 2);
+ ensure(result[0] > 0); // Shortest edge must be in
+
+ // Check component IDs are same
+ size_t compId = 0;
+ for (int i = 0; i < size; ++i) {
+ if (result[i] > 0) {
+ if (compId == 0) compId = result[i];
+ else ensure_equals(result[i], compId);
+ }
+ }
+
+ GEOSFree(result);
+
+ for(auto& input : geoms) {
+ GEOSGeom_destroy(input);
+ }
+}
+
+// Disconnected graph
+template<>
+template<>
+void object::test<2> ()
+{
+ constexpr int size = 2;
+ GEOSGeometry* geoms[size];
+ geoms[0] = GEOSGeomFromWKT("LINESTRING(0 0, 10 0)");
+ geoms[1] = GEOSGeomFromWKT("LINESTRING(20 0, 30 0)");
+
+ size_t* result = GEOSMinimumSpanningTree(geoms, size);
+
+ ensure(nullptr != result);
+ ensure(result[0] > 0);
+ ensure(result[1] > 0);
+ ensure(result[0] != result[1]);
+
+ GEOSFree(result);
+
+ for(auto& input : geoms) {
+ GEOSGeom_destroy(input);
+ }
+}
+
+// Mixed inputs (non-linestrings)
+template<>
+template<>
+void object::test<3> ()
+{
+ constexpr int size = 3;
+ GEOSGeometry* geoms[size];
+ geoms[0] = GEOSGeomFromWKT("LINESTRING(0 0, 10 0)");
+ geoms[1] = GEOSGeomFromWKT("POINT(5 5)");
+ geoms[2] = GEOSGeomFromWKT("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
+
+ size_t* result = GEOSMinimumSpanningTree(geoms, size);
+
+ ensure(nullptr != result);
+ ensure(result[0] > 0);
+ ensure_equals(result[1], 0u);
+ ensure_equals(result[2], 0u);
+
+ GEOSFree(result);
+
+ for(auto& input : geoms) {
+ GEOSGeom_destroy(input);
+ }
+}
+
+// Empty input
+template<>
+template<>
+void object::test<4> ()
+{
+ size_t* result = GEOSMinimumSpanningTree(nullptr, 0);
+ ensure(nullptr == result);
+}
+
+// Null array entries
+template<>
+template<>
+void object::test<5> ()
+{
+ constexpr int size = 2;
+ GEOSGeometry* geoms[size];
+ geoms[0] = GEOSGeomFromWKT("LINESTRING(0 0, 10 0)");
+ geoms[1] = 0;
+
+ size_t* result = GEOSMinimumSpanningTree(geoms, size);
+
+ ensure(nullptr != result);
+ ensure(result[0] > 0);
+ ensure_equals(result[1], 0u);
+
+ GEOSFree(result);
+
+ GEOSGeom_destroy(geoms[0]);
+}
+
+// Curved inputs
+template<>
+template<>
+void object::test<6> ()
+{
+ constexpr int size = 2;
+ GEOSGeometry* geoms[size];
+ geoms[0] = GEOSGeomFromWKT("CIRCULARSTRING(0 0, 5 5, 10 0)");
+ geoms[1] = GEOSGeomFromWKT("COMPOUNDCURVE((10 0, 10 10), CIRCULARSTRING(10 10, 5 15, 0 10), (0 10, 0 0))");
+
+ size_t* result = GEOSMinimumSpanningTree(geoms, size);
+
+ ensure(nullptr != result);
+ // Forms a loop, MST should pick one.
+
+ int count = 0;
+ for (int i = 0; i < size; ++i) {
+ if (result[i] > 0) count++;
+ }
+ ensure_equals(count, 1);
+
+ GEOSFree(result);
+
+ for(auto& input : geoms) {
+ GEOSGeom_destroy(input);
+ }
+}
+
+} // namespace tut
diff --git a/tests/unit/operation/spanning/SpanningTreeTest.cpp b/tests/unit/operation/spanning/SpanningTreeTest.cpp
new file mode 100644
index 000000000..e12fb6052
--- /dev/null
+++ b/tests/unit/operation/spanning/SpanningTreeTest.cpp
@@ -0,0 +1,308 @@
+//
+// Test Suite for geos::operation::spanning::SpanningTree class.
+
+// tut
+#include <tut/tut.hpp>
+// geos
+#include <geos/operation/spanning/SpanningTree.h>
+#include <geos/io/WKTReader.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Curve.h>
+#include <geos/geom/LineString.h>
+#include <geos/geom/GeometryCollection.h>
+// std
+#include <memory>
+#include <string>
+#include <vector>
+#include <numeric>
+#include <set>
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by tests
+struct test_spanning_data {
+ geos::io::WKTReader wktreader;
+
+ test_spanning_data()
+ : wktreader()
+ {
+ }
+
+ std::unique_ptr<geos::geom::Geometry>
+ readWKT(const std::string& inputWKT)
+ {
+ return std::unique_ptr<geos::geom::Geometry>(wktreader.read(inputWKT));
+ }
+
+ std::vector<const geos::geom::Curve*>
+ toCurves(const geos::geom::Geometry* geom) {
+ std::vector<const geos::geom::Curve*> curves;
+ if (const geos::geom::GeometryCollection* gc = dynamic_cast<const geos::geom::GeometryCollection*>(geom)) {
+ for (std::size_t i = 0; i < gc->getNumGeometries(); ++i) {
+ if (const geos::geom::Curve* c = dynamic_cast<const geos::geom::Curve*>(gc->getGeometryN(i))) {
+ curves.push_back(c);
+ }
+ }
+ } else if (const geos::geom::Curve* c = dynamic_cast<const geos::geom::Curve*>(geom)) {
+ curves.push_back(c);
+ }
+ return curves;
+ }
+};
+
+typedef test_group<test_spanning_data> group;
+typedef group::object object;
+
+group test_spanning_group("geos::operation::spanning::SpanningTree");
+
+//
+// Test Cases
+//
+
+// Basic triangle test
+template<> template<>
+void object::test<1>()
+{
+ // Triangle with side lengths 10, ~11.18, ~11.18
+ // MST should pick the shortest edge (10) and one of the others.
+ // Lengths:
+ // 0: (0 0, 10 0) -> 10
+ // 1: (10 0, 5 10) -> sqrt(25+100) = 11.18
+ // 2: (5 10, 0 0) -> sqrt(25+100) = 11.18
+
+ std::string wkt = "MULTILINESTRING((0 0, 10 0), (10 0, 5 10), (5 10, 0 0))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 3u);
+
+ // Count edges in MST (should be 2)
+ int count = 0;
+ for (std::size_t r : result) {
+ if (r > 0) count++;
+ }
+ ensure_equals(count, 2);
+
+ // Edge 0 (length 10) must be included
+ ensure(result[0] > 0);
+
+ // Component IDs should be the same (connected graph)
+ std::size_t compId = result[0];
+ for (std::size_t r : result) {
+ if (r > 0) ensure_equals(r, compId);
+ }
+}
+
+// Disconnected graph
+template<> template<>
+void object::test<2>()
+{
+ std::string wkt = "MULTILINESTRING((0 0, 10 0), (20 0, 30 0))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 2u);
+ ensure(result[0] > 0);
+ ensure(result[1] > 0);
+
+ // Should be different components
+ ensure(result[0] != result[1]);
+}
+
+// Complex graph
+template<> template<>
+void object::test<3>()
+{
+ std::string wkt = "MULTILINESTRING("
+ "(0 0, 10 0), (10 0, 10 10), (10 10, 0 10), (0 10, 0 0), " // 0-3: Sides (10)
+ "(0 0, 10 10), (10 0, 0 10))"; // 4-5: Diagonals (~14.14)
+
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 6u);
+
+ int count = 0;
+ std::set<std::size_t> components;
+ for (std::size_t r : result) {
+ if (r > 0) {
+ count++;
+ components.insert(r);
+ }
+ }
+ // A square with 2 diagonals has 4 vertices. MST should have 3 edges.
+ // Wait, 4 edges for square + 2 diagonals = 6 edges.
+ // Vertices: 4.
+ // MST edges = Vertices - 1 = 3.
+ // result should have 3 non-zero entries.
+ ensure_equals(count, 3);
+
+ // Should be 1 component
+ ensure_equals(components.size(), 1u);
+
+ // Diagonals should not be included as they are longer
+ ensure_equals(result[4], 0u);
+ ensure_equals(result[5], 0u);
+}
+
+// Empty input
+template<> template<>
+void object::test<4>()
+{
+ std::vector<const geos::geom::Curve*> curves;
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+ ensure(result.empty());
+}
+
+// Null pointers and empty geometries
+template<> template<>
+void object::test<5>()
+{
+ auto factory = geos::geom::GeometryFactory::create();
+ auto empty = factory->createLineString();
+
+ std::vector<const geos::geom::Curve*> curves;
+ curves.push_back(nullptr);
+ curves.push_back(empty.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 2u);
+ ensure_equals(result[0], 0u);
+ ensure_equals(result[1], 0u);
+}
+
+// Short lines (zero length) - should be ignored
+template<> template<>
+void object::test<6>()
+{
+ std::string wkt = "MULTILINESTRING((0 0, 0 0))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 1u);
+ ensure_equals(result[0], 0u);
+}
+
+// Loops (start == end)
+template<> template<>
+void object::test<7>()
+{
+ std::string wkt = "MULTILINESTRING((0 0, 10 10, 0 0))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 1u);
+ // A single loop doesn't form a tree with 2 nodes?
+ // Actually, start and end are the same node. So 1 node, 1 edge.
+ // Kruskal: find(start) == find(end), so not included.
+ ensure_equals(result[0], 0u);
+}
+
+// Multi-edges (two edges between same nodes)
+template<> template<>
+void object::test<8>()
+{
+ std::string wkt = "MULTILINESTRING((0 0, 10 0), (0 0, 10 0))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ ensure_equals(result.size(), 2u);
+ // One should be in, one out.
+ int count = 0;
+ for (std::size_t r : result) if (r > 0) count++;
+ ensure_equals(count, 1);
+}
+
+// Mixed Curved geometries
+template<> template<>
+void object::test<9>()
+{
+ std::string wkt = "GEOMETRYCOLLECTION("
+ "CIRCULARSTRING(0 0, 5 5, 10 0),"
+ "LINESTRING(10 0, 10 10),"
+ "COMPOUNDCURVE(CIRCULARSTRING(10 10, 5 15, 0 10), (0 10, 0 0))"
+ ")";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+ ensure_equals(curves.size(), 3u);
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+
+ int count = 0;
+ for (std::size_t r : result) if (r > 0) count++;
+
+ // Debug:
+ // for (std::size_t i = 0; i < curves.size(); ++i) {
+ // std::cout << "Curve " << i << " length=" << curves[i]->getLength() << " result=" << result[i] << std::endl;
+ // }
+
+ // Geometries:
+ // C0: (0,0) -> (10,0) via (5,5). Length: PI*5 ~= 15.7
+ // C1: (10,0) -> (10,10). Length: 10
+ // C2: (10,10) -> (0,0) via (5,15) and (0,10).
+ // Part 1: (10,10) -> (0,10) via (5,15). Length: PI*5 ~= 15.7
+ // Part 2: (0,10) -> (0,0). Length: 10
+ // Total C2 length ~= 25.7
+
+ // Nodes: (0,0), (10,0), (10,10).
+ // Wait, (0,10) is NOT a node because it's INTERNAL to COMPOUNDCURVE C2.
+ // So we have 3 nodes: N0(0,0), N1(10,0), N2(10,10).
+ // Edges:
+ // E0: N0-N1 (Length 15.7)
+ // E1: N1-N2 (Length 10)
+ // E2: N2-N0 (Length 25.7)
+
+ // MST should pick E1 (10) and E0 (15.7). Total 2 edges.
+ // My expectation of 3 was wrong because I thought there were 4 nodes.
+ ensure_equals(count, 2);
+}
+
+// Simple square
+template<> template<>
+void object::test<10>()
+{
+ std::string wkt = "MULTILINESTRING((0 0,1 0),(1 1,1 0),(0 0,1 1),(0 0,0 1),(1 1,0 1),(1 0,0 1))";
+ auto geom = readWKT(wkt);
+ auto curves = toCurves(geom.get());
+
+ std::vector<std::size_t> result;
+ geos::operation::spanning::SpanningTree::mst(curves, result);
+ ensure_equals(result.size(), 6u);
+ // MULTILINESTRING((0 0,1 0),(1 1,1 0),(0 0,0 1))
+
+ int count = 0;
+ for (std::size_t r : result) {
+ //std::cout << r << std::endl;
+ if (r > 0) count++;
+ }
+ ensure_equals(count, 3);
+}
+
+
+} // namespace tut
commit 224d1448e5086c2f0017c4c58b05a25823d27173
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Tue Mar 31 15:16:07 2026 -0700
Squashed commit of the following:
commit b3c8a8b4d269c9359c3cec2f5fdfac4e6626d3ec
Merge: cc404b809 8b2ee5e04
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Tue Mar 31 15:11:14 2026 -0700
Merge branch 'main' into main-coverage-edges
commit cc404b80924b97eed4b3ff43ea0c7ae312696b32
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Tue Mar 31 13:44:42 2026 -0700
change API slightly
commit bb1bfbcc25ae194d82d32a4d108102959fa9f804
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Mon Mar 30 16:59:09 2026 -0700
CoverageEdges first draft
Add
extern GEOSGeometry GEOS_DLL *GEOSCoverageEdges(const GEOSGeometry *input, int edgetype)
which returns the edges of a polygonal coverage. With edgetype == 0 all edges
are returned, edgetype == 1 just exterior, edgetype == 2 just interior edges.
diff --git a/NEWS.md b/NEWS.md
index 3f8bf9b43..79fb62aa5 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -3,6 +3,7 @@
2026-xx-xx
- New things:
+ - Add GEOSCoverageEdges (Paul Ramsey)
- Add new C API functions for Hausdorff distance (GH-1352, Sven Jensen)
- Add GEOSSubdivideByGrid (GH-1232, Dan Baston)
diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index e74070e3e..54d29bba9 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -1100,6 +1100,15 @@ extern "C" {
handle, input);
}
+ GEOSGeometry *
+ GEOSCoverageEdges(
+ const GEOSGeometry * input,
+ int edgetype)
+ {
+ return GEOSCoverageEdges_r(
+ handle, input, edgetype);
+ }
+
Geometry*
GEOSRemoveRepeatedPoints(
const Geometry* g,
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 1116b0024..b38029835 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -938,6 +938,13 @@ GEOSCoverageSimplifyVW_r(
double tolerance,
int preserveBoundary);
+/** \see GEOSCoverageEdges */
+extern GEOSGeometry GEOS_DLL *
+GEOSCoverageEdges_r(
+ GEOSContextHandle_t handle,
+ const GEOSGeometry* input,
+ int edgetype);
+
/** \see GEOSCoverageCleanParams_create */
extern GEOSCoverageCleanParams GEOS_DLL *
GEOSCoverageCleanParams_create_r(
@@ -4630,8 +4637,18 @@ extern GEOSGeometry GEOS_DLL * GEOSCoverageSimplifyVW(
int preserveBoundary);
/**
-* Create a default GEOSCoverageCleanParams object for controlling
-* the way invalid polygon interactions are repaired by \ref GEOSCoverageCleanWithParams.
+ * Returns a MultiLineString representing the unique edges of a polygonal coverage.
+ *
+ * \param input a GeometryCollection or MultiPolygon representing the coverage.
+ * \param edgetype selection type: 0 = ALL, 1 = EXTERIOR (non-shared), 2 = INTERIOR (shared).
+ * \return a MultiLineString of unique edges, or NULL on error.
+ *
+ * \since 3.15
+ */
+extern GEOSGeometry GEOS_DLL *GEOSCoverageEdges(const GEOSGeometry *input, int edgetype);
+
+/**
+ * Create a default GEOSCoverageCleanParams object for controlling* the way invalid polygon interactions are repaired by \ref GEOSCoverageCleanWithParams.
* \return A newly allocated GEOSCoverageCleanParams. NULL on exception.
* Caller is responsible for freeing with GEOSCoverageCleanParams_destroy().
*
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index e57a00343..05c83acdf 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -28,6 +28,7 @@
#include <geos/algorithm/distance/DiscreteFrechetDistance.h>
#include <geos/algorithm/hull/ConcaveHull.h>
#include <geos/algorithm/hull/ConcaveHullOfPolygons.h>
+#include <geos/coverage/CoverageEdges.h>
#include <geos/coverage/CoverageCleaner.h>
#include <geos/coverage/CoverageSimplifier.h>
#include <geos/coverage/CoverageUnion.h>
@@ -4817,5 +4818,16 @@ extern "C" {
return GEOSCoverageCleanWithParams_r(extHandle, input, nullptr);
}
+ GEOSGeometry *
+ GEOSCoverageEdges_r(GEOSContextHandle_t extHandle,
+ const GEOSGeometry* input,
+ int edgetype)
+ {
+ using geos::coverage::CoverageEdges;
+
+ return execute(extHandle, [&]() -> Geometry* {
+ return CoverageEdges::GetEdges(input, edgetype).release();
+ });
+ }
} /* extern "C" */
diff --git a/include/geos/coverage/CoverageEdges.h b/include/geos/coverage/CoverageEdges.h
new file mode 100644
index 000000000..b1b6a847a
--- /dev/null
+++ b/include/geos/coverage/CoverageEdges.h
@@ -0,0 +1,61 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Geometry;
+}
+}
+
+namespace geos { // geos
+namespace coverage { // geos::coverage
+
+/**
+ * Utility to extract unique edges from a polygonal coverage.
+ *
+ * @author Paul Ramsey
+ *
+ */
+class GEOS_DLL CoverageEdges {
+public:
+
+ /**
+ * Returns a MultiLineString representing the unique edges of a polygonal coverage.
+ *
+ * @param coverage a vector of polygons in the coverage
+ * @param type selection type: 0 = ALL, 1 = EXTERIOR (non-shared), 2 = INTERIOR (shared)
+ * @return a MultiLineString of unique edges
+ */
+ static std::unique_ptr<geom::Geometry> GetEdges(const std::vector<const geom::Geometry*>& coverage, int type = 0);
+
+ /**
+ * Returns a MultiLineString representing the unique edges of a polygonal coverage.
+ *
+ * @param coverage a GeometryCollection or MultiPolygon representing the coverage
+ * @param type selection type: 0 = ALL, 1 = EXTERIOR (non-shared), 2 = INTERIOR (shared)
+ * @return a MultiLineString of unique edges
+ */
+ static std::unique_ptr<geom::Geometry> GetEdges(const geom::Geometry* coverage, int type = 0);
+
+};
+
+} // namespace geos::coverage
+} // namespace geos
diff --git a/src/coverage/CoverageEdges.cpp b/src/coverage/CoverageEdges.cpp
new file mode 100644
index 000000000..3c2dd6933
--- /dev/null
+++ b/src/coverage/CoverageEdges.cpp
@@ -0,0 +1,76 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/coverage/CoverageEdges.h>
+#include <geos/coverage/CoverageRingEdges.h>
+#include <geos/coverage/CoverageEdge.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiLineString.h>
+#include <geos/geom/util/PolygonExtracter.h>
+
+namespace geos { // geos
+namespace coverage { // geos::coverage
+
+/* public static */
+std::unique_ptr<geom::Geometry>
+CoverageEdges::GetEdges(const std::vector<const geom::Geometry*>& coverage, int type)
+{
+ if (coverage.empty()) {
+ return nullptr;
+ }
+
+ const geom::GeometryFactory* factory = coverage[0]->getFactory();
+ CoverageRingEdges cre(coverage);
+ std::vector<CoverageEdge*> selectedEdges;
+
+ for (auto* edge : cre.getEdges()) {
+ if (type == 2) { // INTERIOR
+ if (edge->getRingCount() > 1)
+ selectedEdges.push_back(edge);
+ }
+ else if (type == 1) { // EXTERIOR
+ if (edge->getRingCount() == 1)
+ selectedEdges.push_back(edge);
+ }
+ else { // ALL (0 and default)
+ selectedEdges.push_back(edge);
+ }
+ }
+
+ return CoverageEdge::createLines(selectedEdges, factory);
+}
+
+/* public static */
+std::unique_ptr<geom::Geometry>
+CoverageEdges::GetEdges(const geom::Geometry* input, int type)
+{
+ std::vector<const geom::Polygon*> polygons;
+ geom::util::PolygonExtracter::getPolygons(*input, polygons);
+
+ if (polygons.empty()) {
+ return input->getFactory()->createMultiLineString();
+ }
+
+ std::vector<const geom::Geometry*> coverage;
+ for (const auto* poly : polygons) {
+ coverage.push_back(poly);
+ }
+
+ return GetEdges(coverage, type);
+}
+
+
+} // namespace geos::coverage
+} // namespace geos
diff --git a/tests/unit/capi/GEOSCoverageEdgesTest.cpp b/tests/unit/capi/GEOSCoverageEdgesTest.cpp
new file mode 100644
index 000000000..c2ca7046d
--- /dev/null
+++ b/tests/unit/capi/GEOSCoverageEdgesTest.cpp
@@ -0,0 +1,51 @@
+//
+// Test Suite for GEOSCoverageEdges CAPI function.
+
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_geoscoverageedges_data : public capitest::utility
+ {
+ test_geoscoverageedges_data() {}
+};
+
+typedef test_group<test_geoscoverageedges_data> group;
+typedef group::object object;
+
+group test_geoscoverageedges_data("capi::GEOSCoverageEdges");
+
+
+// testTwoAdjacentInterior
+template<>
+template<>
+void object::test<1> ()
+{
+ input_ = fromWKT("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))");
+ result_ = GEOSCoverageEdges(input_, 2); // INTERIOR
+ expected_ = fromWKT("MULTILINESTRING ((1 6, 6 5))");
+
+ ensure_geometry_equals(result_, expected_);
+}
+
+// testTwoAdjacentExterior
+template<>
+template<>
+void object::test<2> ()
+{
+ input_ = fromWKT("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))");
+ result_ = GEOSCoverageEdges(input_, 1); // EXTERIOR
+ expected_ = fromWKT("MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5))");
+
+ ensure_geometry_equals(result_, expected_);
+}
+
+} // namespace tut
diff --git a/tests/unit/coverage/CoverageEdgesTest.cpp b/tests/unit/coverage/CoverageEdgesTest.cpp
new file mode 100644
index 000000000..1f3f520da
--- /dev/null
+++ b/tests/unit/coverage/CoverageEdgesTest.cpp
@@ -0,0 +1,113 @@
+//
+// Test Suite for geos::coverage::CoverageEdges class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/coverage/CoverageEdges.h>
+#include <geos/geom/Geometry.h>
+
+using geos::coverage::CoverageEdges;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_coverageedges_data
+ {
+
+ WKTReader r;
+ WKTWriter w;
+
+ test_coverageedges_data() {
+ }
+
+ void
+ checkEdges(const std::string& wkt, int type, const std::string& wktExpected)
+ {
+ std::unique_ptr<Geometry> geom = r.read(wkt);
+ std::unique_ptr<Geometry> edgeLines = CoverageEdges::GetEdges(geom.get(), type);
+ std::unique_ptr<Geometry> expected = r.read(wktExpected);
+ ensure_equals_geometry(edgeLines.get(), expected.get());
+ }
+};
+
+
+typedef test_group<test_coverageedges_data> group;
+typedef group::object object;
+
+group test_coverageedges_data("geos::coverage::CoverageEdges");
+
+
+// testTwoAdjacentAll
+template<>
+template<>
+void object::test<1> ()
+{
+ checkEdges(
+ "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))",
+ 0,
+ "MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5), (1 6, 6 5))"
+ );
+}
+
+// testTwoAdjacentInterior
+template<>
+template<>
+void object::test<2> ()
+{
+ checkEdges(
+ "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))",
+ 2, // INTERIOR
+ "MULTILINESTRING ((1 6, 6 5))"
+ );
+}
+
+// testTwoAdjacentExterior
+template<>
+template<>
+void object::test<3> ()
+{
+ checkEdges(
+ "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))",
+ 1, // EXTERIOR
+ "MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5))"
+ );
+}
+
+// testAdjacentSquaresInterior
+template<>
+template<>
+void object::test<4> ()
+{
+ std::string wkt = "GEOMETRYCOLLECTION (POLYGON ((1 3, 2 3, 2 2, 1 2, 1 3)), POLYGON ((3 3, 3 2, 2 2, 2 3, 3 3)), POLYGON ((3 1, 2 1, 2 2, 3 2, 3 1)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))";
+ checkEdges(wkt, 2, // INTERIOR
+ "MULTILINESTRING ((1 2, 2 2), (2 1, 2 2), (2 2, 2 3), (2 2, 3 2))");
+}
+
+// testTouchingAtPointInterior
+template<>
+template<>
+void object::test<5> ()
+{
+ // Two polygons touching only at a point (1 1)
+ std::string wkt = "GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))";
+ checkEdges(wkt, 2, // INTERIOR
+ "MULTILINESTRING EMPTY");
+}
+
+// testTouchingAtPointExterior
+template<>
+template<>
+void object::test<6> ()
+{
+ // Two polygons touching only at a point (1 1)
+ std::string wkt = "GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))";
+ checkEdges(wkt, 1, // EXTERIOR
+ "MULTILINESTRING ((0 0, 0 1, 1 1, 1 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))");
+}
+
+} // namespace tut
-----------------------------------------------------------------------
Summary of changes:
NEWS.md | 1 +
capi/geos_c.cpp | 15 +
capi/geos_c.h.in | 48 +++-
capi/geos_ts_c.cpp | 53 ++++
include/geos/coverage/CoverageEdges.h | 61 ++++
include/geos/operation/spanning/SpanningTree.h | 59 ++++
src/coverage/CoverageEdges.cpp | 76 +++++
src/operation/spanning/SpanningTree.cpp | 159 +++++++++++
tests/unit/capi/GEOSCoverageEdgesTest.cpp | 51 ++++
tests/unit/capi/GEOSMinimumSpanningTreeTest.cpp | 175 ++++++++++++
tests/unit/coverage/CoverageEdgesTest.cpp | 113 ++++++++
tests/unit/operation/spanning/SpanningTreeTest.cpp | 308 +++++++++++++++++++++
12 files changed, 1115 insertions(+), 4 deletions(-)
create mode 100644 include/geos/coverage/CoverageEdges.h
create mode 100644 include/geos/operation/spanning/SpanningTree.h
create mode 100644 src/coverage/CoverageEdges.cpp
create mode 100644 src/operation/spanning/SpanningTree.cpp
create mode 100644 tests/unit/capi/GEOSCoverageEdgesTest.cpp
create mode 100644 tests/unit/capi/GEOSMinimumSpanningTreeTest.cpp
create mode 100644 tests/unit/coverage/CoverageEdgesTest.cpp
create mode 100644 tests/unit/operation/spanning/SpanningTreeTest.cpp
hooks/post-receive
--
GEOS
More information about the geos-commits
mailing list