Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- Add `geos.ClipByRect` function that clips a geometry to an axis-aligned
rectangle (defined by a `geom.Envelope`). This wraps the GEOS
`GEOSClipByRect` operation, which is faster than computing a full
`Intersection` with a rectangular polygon.

## v0.58.0

2026-02-15
Expand Down
13 changes: 13 additions & 0 deletions geos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,16 @@ func UnaryUnion(g geom.Geometry) (geom.Geometry, error) {
func ConcaveHull(g geom.Geometry, concavenessRatio float64, allowHoles bool) (geom.Geometry, error) {
return rawgeos.ConcaveHull(g, concavenessRatio, allowHoles)
}

// ClipByRect clips a geometry to an axis-aligned rectangle defined by the
// given [geom.Envelope]. If the envelope is empty, then an empty
// [geom.GeometryCollection] is returned.
//
// The validity of the result is not checked.
func ClipByRect(g geom.Geometry, rect geom.Envelope) (geom.Geometry, error) {
lo, hi, ok := rect.MinMaxXYs()
if !ok {
return geom.Geometry{}, nil
}
return rawgeos.ClipByRect(g, lo.X, lo.Y, hi.X, hi.Y)
Comment thread
peterstace marked this conversation as resolved.
}
65 changes: 65 additions & 0 deletions geos/entrypoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,3 +1016,68 @@ func TestCoverageIsValid(t *testing.T) {
})
}
}

func TestClipByRect(t *testing.T) {
for _, tc := range []struct {
name string
input string
rect geom.Envelope
want string
}{
{
name: "polygon fully inside rect",
input: "POLYGON((1 1,1 2,2 2,2 1,1 1))",
rect: geom.NewEnvelope(geom.XY{X: 0, Y: 0}, geom.XY{X: 3, Y: 3}),
want: "POLYGON((1 1,1 2,2 2,2 1,1 1))",
},
{
name: "polygon partially overlapping rect",
input: "POLYGON((0 0,0 4,4 4,4 0,0 0))",
rect: geom.NewEnvelope(geom.XY{X: 1, Y: 1}, geom.XY{X: 3, Y: 3}),
want: "POLYGON((1 1,1 3,3 3,3 1,1 1))",
},
{
name: "polygon fully outside rect",
input: "POLYGON((0 0,0 1,1 1,1 0,0 0))",
rect: geom.NewEnvelope(geom.XY{X: 5, Y: 5}, geom.XY{X: 6, Y: 6}),
want: "GEOMETRYCOLLECTION EMPTY",
},
{
name: "linestring clipped by rect",
input: "LINESTRING(0 0,4 4)",
rect: geom.NewEnvelope(geom.XY{X: 1, Y: 1}, geom.XY{X: 3, Y: 3}),
want: "LINESTRING(1 1,3 3)",
},
{
name: "point inside rect",
input: "POINT(2 2)",
rect: geom.NewEnvelope(geom.XY{X: 1, Y: 1}, geom.XY{X: 3, Y: 3}),
want: "POINT(2 2)",
},
{
name: "point outside rect",
input: "POINT(0 0)",
rect: geom.NewEnvelope(geom.XY{X: 1, Y: 1}, geom.XY{X: 3, Y: 3}),
want: "GEOMETRYCOLLECTION EMPTY",
},
{
name: "empty input geometry",
input: "GEOMETRYCOLLECTION EMPTY",
rect: geom.NewEnvelope(geom.XY{X: 0, Y: 0}, geom.XY{X: 1, Y: 1}),
want: "GEOMETRYCOLLECTION EMPTY",
},
{
name: "empty envelope",
input: "POLYGON((0 0,0 1,1 1,1 0,0 0))",
rect: geom.Envelope{},
want: "GEOMETRYCOLLECTION EMPTY",
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := geos.ClipByRect(geomFromWKT(t, tc.input), tc.rect)
skipIfUnsupported(t, err)
expectNoErr(t, err)
Comment thread
peterstace marked this conversation as resolved.
expectGeomEq(t, got, geomFromWKT(t, tc.want), geom.IgnoreOrder)
})
}
}
22 changes: 22 additions & 0 deletions internal/rawgeos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ GEOSGeometry *GEOSCoverageSimplifyVW_r(GEOSContextHandle_t handle, const GEOSGeo
int GEOSCoverageIsValid_r(GEOSContextHandle_t handle, const GEOSGeometry* g, double gapWidth, GEOSGeometry** invalidEdges) { return 2; }
#endif

#define CLIP_BY_RECT_MIN_VERSION "3.5.0"
#define CLIP_BY_RECT_MISSING ( \
GEOS_VERSION_MAJOR < 3 || \
(GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 5) \
)
#if CLIP_BY_RECT_MISSING
// This stub implementation always fails:
GEOSGeometry *GEOSClipByRect_r(GEOSContextHandle_t handle, const GEOSGeometry* g, double xmin, double ymin, double xmax, double ymax) { return NULL; }
#endif

#define CONCAVE_HULL_MIN_VERSION "3.11.0"
#define CONCAVE_HULL_MISSING ( \
GEOS_VERSION_MAJOR < 3 || \
Expand Down Expand Up @@ -441,6 +451,18 @@ func Envelope(g geom.Geometry) (geom.Geometry, error) {
return result, wrap(err, "executing GEOSEnvelope_r")
}

func ClipByRect(g geom.Geometry, xmin, ymin, xmax, ymax float64) (geom.Geometry, error) {
if C.CLIP_BY_RECT_MISSING != 0 {
return geom.Geometry{}, UnsupportedGEOSVersionError{
C.CLIP_BY_RECT_MIN_VERSION, "ClipByRect",
}
}
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSClipByRect_r(ctx, g, C.double(xmin), C.double(ymin), C.double(xmax), C.double(ymax))
})
return result, wrap(err, "executing GEOSClipByRect_r")
}

func Area(g geom.Geometry) (float64, error) {
result, err := unaryOpF(g, func(h C.GEOSContextHandle_t, g *C.GEOSGeometry, d *C.double) C.int {
return C.GEOSArea_r(h, g, d)
Expand Down
Loading