Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions capi/geos_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,12 @@ extern "C" {
return GEOSNode_r(handle, g);
}

Geometry*
GEOSNodeCollection(const Geometry* input, double gridSize)
{
return GEOSNodeCollection_r(handle, input, gridSize);
}

Geometry*
GEOSSplit(const Geometry* g, const Geometry* edge)
{
Expand Down
47 changes: 47 additions & 0 deletions capi/geos_c.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,12 @@ extern GEOSGeometry GEOS_DLL *GEOSNode_r(
GEOSContextHandle_t handle,
const GEOSGeometry* g);

/** \see GEOSNodeCollection */
extern GEOSGeometry GEOS_DLL *GEOSNodeCollection_r(
GEOSContextHandle_t handle,
const GEOSGeometry* input,
double gridSize);

/** \see GEOSSplit */
extern GEOSGeometry GEOS_DLL *GEOSSplit_r(
GEOSContextHandle_t handle,
Expand Down Expand Up @@ -5563,6 +5569,47 @@ extern GEOSGeometry GEOS_DLL * GEOSVoronoiDiagram(
extern GEOSGeometry GEOS_DLL *GEOSNode(const GEOSGeometry* g);


/**
* Nodes a collection of geometries against each other, returning a
* GeometryCollection of the same size in which member i is the noded
* form of input member i.
*
* Unlike GEOSNode(), which collects all edges into a single flattened
* result, this preserves the per-member structure of the input:
* linework shared (or nearly shared) between members is not dissolved,
* and every member that touches a node is split there.
*
* Each output member is a MultiLineString (or a MultiCurve, when the
* corresponding input contributed curved components). Areal members are
* reduced to their boundary linework — a polygon's rings are noded like
* any other lines — so a polygon member yields its noded boundary. Point
* members (and any member with no linear or areal component) yield an
* empty MultiLineString.
*
* A non-collection input (for example a single LineString or Polygon) is
* treated as a one-member collection, producing a one-member result.
*
* When \p gridSize is 0.0 (or any non-positive value), exact noding is
* used. When \p gridSize is greater than 0.0, snap-rounding to that grid
* is used for robust output on inputs with near-coincident coordinates.
* Snap-rounding is linear-only: if the input contains curved components,
* \p gridSize is ignored and exact arc noding is used.
*
* \param input A collection of geometries to node against each other.
* \param gridSize Snap-rounding grid size when greater than 0; any
* non-positive value selects exact noding.
* \return A GeometryCollection of the same size as the input, or NULL on
* exception. Caller is responsible for freeing with
* GEOSGeom_destroy().
* \see geos::noding::GeometryNoder::nodeCollection
*
* \since 3.15
*/
extern GEOSGeometry GEOS_DLL *GEOSNodeCollection(
const GEOSGeometry* input,
double gridSize);


/** Split a linear or polygonal input
*
* Linear inputs can be split with points, lines, and/or polygons.
Expand Down
20 changes: 20 additions & 0 deletions capi/geos_ts_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,26 @@ extern "C" {
});
}

Geometry*
GEOSNodeCollection_r(GEOSContextHandle_t extHandle, const Geometry* input, double gridSize)
{
return execute(extHandle, [&]() {
// Each member of the input collection is noded against the
// others; the result is a collection of the same size, member
// i being the noded form of input member i.
std::vector<const Geometry*> geoms(input->getNumGeometries());
for (std::size_t i = 0; i < geoms.size(); i++) {
geoms[i] = input->getGeometryN(i);
}

auto noded = geos::noding::GeometryNoder::nodeCollection(geoms, gridSize);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the conversion to a vector buy anything here? It seems like nodeCollection could take a collection as an argument instead of a vector. That would also clean up GeometryNoder by not having both argGeom1 and argColl members, and would remove the need for the collectionHasCurves helper.

@pramsey pramsey Jun 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CoverageSimplifier is the analogue I was looking at, which has

std::vector<std::unique_ptr<Geometry>>
CoverageSimplifier::simplify(
    std::vector<const Geometry*>& coverage,
    double tolerance)

as its main entry point. On the flip in the C API the entry point is

    Geometry*
    GEOSCoverageSimplifyVW(
        const Geometry* input,
        double tolerance,
        int preserveBoundary)

so.... what would you prefer?

@pramsey pramsey Jun 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

geom.hasCurvedComponents() in place of collectionHasCurves() I presume

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong preference I guess, it would just be nice if there was some way to not have all of argGeom1, argGeom2, and argColl (whose name implies a collection but is not...)


auto out = input->getFactory()->createGeometryCollection(std::move(noded));
out->setSRID(input->getSRID());
return out.release();
});
}

Geometry*
GEOSSplit_r(GEOSContextHandle_t extHandle, const Geometry* g, const Geometry* edge)
{
Expand Down
72 changes: 72 additions & 0 deletions include/geos/noding/GeometryNoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
#include <geos/export.h>
#include <geos/noding/SegmentString.h> // for NonConstVect

#include <cstddef>
#include <map>
#include <memory> // for unique_ptr
#include <vector>

// Forward declarations
namespace geos {
Expand All @@ -30,6 +33,7 @@ class CircularArcIntersector;
}
namespace geom {
class Geometry;
class PrecisionModel;
}
namespace noding {
class ArcIntersectionAdder;
Expand All @@ -47,14 +51,58 @@ class GEOS_DLL GeometryNoder {

static std::unique_ptr<geom::Geometry> node(const geom::Geometry& geom1, const geom::Geometry& geom2);

/**
* Nodes a collection of linear (or curved) geometries against each
* other, returning a collection of the same size with a 1:1
* relationship to the input: output element `i` is the noded form
* of input element `i`.
*
* Unlike node(const geom::Geometry&), which collects all input
* edges into a single flattened result, this preserves the identity
* of each input member. Linework that is shared (or nearly shared)
* between members is not dissolved — every member that touches a
* node is split there.
*
* Each output element is a MultiLineString (or a MultiCurve, if the
* corresponding input contributed curved components). Consistent with
* node(const geom::Geometry&), areal members are reduced to their
* boundary linework: a polygon's exterior and interior rings are noded
* like any other lines (and participate in noding the other members),
* so its slot returns the noded boundary as a MultiLineString. Point
* members — and any member with no linear or areal component — add
* nothing to the noding and yield an empty MultiLineString in their slot.
*
* When `gridSize <= 0.0` (the default is 0.0) exact noding is used
* (IteratedNoder for linear input, SimpleNoder with arc support when
* curves are present). When `gridSize > 0.0` a SnapRoundingNoder is
* used for robust output on near-coincident coordinates
* (see https://github.com/libgeos/geos/issues/877). Snap-rounding is
* linear-only: if the input contains curved components, `gridSize`
* is ignored and exact arc noding is used.
*
* @param geoms input members; the caller retains ownership and the
* vector (and the geometries it points at) must outlive the call.
* Members must be distinct geometry objects (no aliasing).
* @param gridSize snap-rounding grid size when > 0; any value <= 0.0
* selects exact noding
* @return one noded geometry per input member, in input order
*/
static std::vector<std::unique_ptr<geom::Geometry>> nodeCollection(
const std::vector<const geom::Geometry*>& geoms,
double gridSize = 0.0);

GeometryNoder(const geom::Geometry& g);

GeometryNoder(const geom::Geometry& g1, const geom::Geometry& g2);

GeometryNoder(const std::vector<const geom::Geometry*>& geoms, double gridSize = 0.0);

~GeometryNoder();

std::unique_ptr<geom::Geometry> getNoded();

std::vector<std::unique_ptr<geom::Geometry>> getNodedCollection();

void setOnlyFirstGeomEdges(bool onlyFirstGeomEdges);

void setPreserveCompoundCurves(bool preserve);
Expand All @@ -67,14 +115,22 @@ class GEOS_DLL GeometryNoder {

bool isInResult(const PathString& ps) const;

static bool collectionHasCurves(const std::vector<const geom::Geometry*>& geoms);

const geom::Geometry* argGeom1;
const geom::Geometry* argGeom2;
const bool argGeomHasCurves;
bool argGeomHasCompoundCurves;
bool onlyFirstGeomEdges;
bool preserveCompoundCurves;

// Collection-noding state (null/empty for single/two-geometry modes)
const std::vector<const geom::Geometry*>* argColl = nullptr;
double m_gridSize = 0.0;
std::map<const void*, std::size_t> m_contextToMember;

std::unique_ptr<Noder> noder;
std::unique_ptr<geom::PrecisionModel> m_pm;
std::unique_ptr<algorithm::CircularArcIntersector> m_cai;
std::unique_ptr<ArcIntersectionAdder> m_aia;

Expand All @@ -84,6 +140,22 @@ class GEOS_DLL GeometryNoder {

std::unique_ptr<geom::Geometry> toGeometry(std::vector<std::unique_ptr<PathString>>& noded) const;

std::vector<std::unique_ptr<geom::Geometry>> toGeometryCollection(
std::vector<std::unique_ptr<PathString>>& noded) const;

/**
* Builds one output geometry for a single source geometry slot from
* the noded paths selected for it. `selected` are the candidate
* paths (deduplicated here); `blockers` are paths from other slots
* whose endpoints must not be merged across when reconstructing
* compound curves. `srcGeom` is the originating geometry (used for
* its factory and, when preserving compound curves, its structure).
*/
std::unique_ptr<geom::Geometry> buildSlot(
const std::vector<PathString*>& selected,
const std::vector<PathString*>& blockers,
const geom::Geometry& srcGeom) const;

};

} // namespace geos.noding
Expand Down
Loading
Loading