From 78c72a0ca42800328c6ba96543a0efec2b41fb24 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Wed, 12 Aug 2026 16:11:36 +1000 Subject: [PATCH 1/3] Finish NewEnvelopeXY constructor NewEnvelopeXY landed early as part of the test helper consolidation in #707, without the finishing that the rest of the XY constructor family has. Move it out of the middle of the Polygon block, where it separated NewPolygonXYZM from NewSingleRingPolygonXY, to sit after the geometry type constructors. Document that the result is the smallest Envelope containing all of the coordinates, which the variadic signature doesn't convey, and add the note about validation that every sibling constructor carries. Envelope has a Validate method, so the note applies here too. Closes #710 --- CHANGELOG.md | 8 ++++++-- geom/ctor_from_coords.go | 32 ++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c49802..939be5c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ## Unreleased -- Add `NewEnvelopeXY` constructor for building an `Envelope` from variadic x/y - coordinate pairs, following the existing `New*XY` constructor pattern. +- Add `NewEnvelopeXY` constructor, which builds an `Envelope` from variadic x + and y coordinates (x1, y1, x2, y2, ..., xn, yn), where `NewEnvelope` takes + `XY` values. It joins the other `XY` constructors such as `NewPointXY` and + `NewLineStringXY`. The result is the smallest `Envelope` containing all of the + coordinates, so no arguments gives the empty envelope and a single pair gives + a point envelope. An odd number of arguments panics. ## v0.59.0 diff --git a/geom/ctor_from_coords.go b/geom/ctor_from_coords.go index 916cc76d..0956b10e 100644 --- a/geom/ctor_from_coords.go +++ b/geom/ctor_from_coords.go @@ -227,20 +227,6 @@ func NewPolygonXYZM(xyzms ...[]float64) Polygon { return polygonFromCoords(xyzms, DimXYZM) } -// NewEnvelopeXY builds a new [Envelope] from x and y coordinates, x1, y1, x2, -// y2, ..., xn, yn. If the number of coordinates is not a multiple of 2 the -// function will panic. -func NewEnvelopeXY(xys ...float64) Envelope { - if len(xys)%2 != 0 { - panic("geom: coordinate arguments to NewEnvelopeXY must have a length that is a multiple of 2") - } - var env Envelope - for i := 0; i < len(xys); i += 2 { - env = env.ExpandToIncludeXY(XY{xys[i], xys[i+1]}) - } - return env -} - // NewSingleRingPolygonXY builds a new XY [Polygon] from the x and y coordinates // of its exterior ring, in the form x1, y1, x2, y2, ..., xn, yn, x1, y1 (the // first and last coordinates of the ring should be the same). If the number of @@ -343,6 +329,24 @@ func NewMultiPolygonXYZM(xyzms ...[][]float64) MultiPolygon { return multiPolygonFromCoords(xyzms, DimXYZM) } +// NewEnvelopeXY builds a new [Envelope] from x and y coordinates, x1, y1, x2, +// y2, ..., xn, yn. The result is the smallest [Envelope] containing all of +// those coordinates. If the number of coordinates is not a multiple of 2 the +// function will panic. +// +// It doesn't perform any validation on the result. The [Envelope.Validate] method can be +// used to check the validity of the result if needed. +func NewEnvelopeXY(xys ...float64) Envelope { + if len(xys)%2 != 0 { + panic("geom: coordinate arguments to NewEnvelopeXY must have a length that is a multiple of 2") + } + var env Envelope + for i := 0; i < len(xys); i += 2 { + env = env.ExpandToIncludeXY(XY{xys[i], xys[i+1]}) + } + return env +} + func clone1DFloat64s(src []float64) []float64 { // TODO: Use slices.Clone once on Go 1.21. if len(src) == 0 { From 7d4a862e56954fb13607798104b6a40d1672b753 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Wed, 12 Aug 2026 16:11:46 +1000 Subject: [PATCH 2/3] Use NewEnvelopeXY for envelopes built from literals The ClipByRect test cases spelled out two XY literals with named fields for each rectangle, which is the verbosity NewEnvelopeXY exists to avoid. The TWKB bbox header and the zig zag ring benchmark had already computed their bounds as float64s, so they were wrapping them in XY values only to hand them to NewEnvelope. Call sites that hold XY values already, such as the type_envelope tests that exercise NewEnvelope itself, are left alone. --- geom/perf_test.go | 2 +- geom/twkb_parser.go | 8 ++++---- geos/entrypoints_test.go | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/geom/perf_test.go b/geom/perf_test.go index 91429fc6..4c81d8e4 100644 --- a/geom/perf_test.go +++ b/geom/perf_test.go @@ -167,7 +167,7 @@ func BenchmarkPolygonMultipleRingsValidation(b *testing.B) { func BenchmarkPolygonZigZagRingsValidation(b *testing.B) { for _, sz := range []int{10, 100, 1000, 10000} { b.Run(fmt.Sprintf("n=%d", sz), func(b *testing.B) { - outerRingEnv := geom.NewEnvelope(geom.XY{}, geom.XY{7, float64(sz + 1)}) + outerRingEnv := geom.NewEnvelopeXY(0, 0, 7, float64(sz+1)) outerRing := outerRingEnv.AsGeometry().MustAsPolygon().ExteriorRing() var leftFloats, rightFloats []float64 for i := 0; i < sz; i++ { diff --git a/geom/twkb_parser.go b/geom/twkb_parser.go index 57ff8b23..125d8087 100644 --- a/geom/twkb_parser.go +++ b/geom/twkb_parser.go @@ -346,7 +346,7 @@ func (p *twkbParser) parseBBoxHeader() (ExtendedEnvelope, error) { maxM := float64(p.bbox[6]+p.bbox[7]) / p.scalings[3] return ExtendedEnvelope{ - XYEnvelope: NewEnvelope(XY{minX, minY}, XY{maxX, maxY}), + XYEnvelope: NewEnvelopeXY(minX, minY, maxX, maxY), ZRange: NewInterval(minZ, maxZ), MRange: NewInterval(minM, maxM), }, nil @@ -360,7 +360,7 @@ func (p *twkbParser) parseBBoxHeader() (ExtendedEnvelope, error) { maxZ := float64(p.bbox[4]+p.bbox[5]) / p.scalings[2] return ExtendedEnvelope{ - XYEnvelope: NewEnvelope(XY{minX, minY}, XY{maxX, maxY}), + XYEnvelope: NewEnvelopeXY(minX, minY, maxX, maxY), ZRange: NewInterval(minZ, maxZ), }, nil case p.hasM: @@ -373,7 +373,7 @@ func (p *twkbParser) parseBBoxHeader() (ExtendedEnvelope, error) { maxM := float64(p.bbox[4]+p.bbox[5]) / p.scalings[2] return ExtendedEnvelope{ - XYEnvelope: NewEnvelope(XY{minX, minY}, XY{maxX, maxY}), + XYEnvelope: NewEnvelopeXY(minX, minY, maxX, maxY), MRange: NewInterval(minM, maxM), }, nil default: @@ -384,7 +384,7 @@ func (p *twkbParser) parseBBoxHeader() (ExtendedEnvelope, error) { maxY := float64(p.bbox[2]+p.bbox[3]) / p.scalings[1] return ExtendedEnvelope{ - XYEnvelope: NewEnvelope(XY{minX, minY}, XY{maxX, maxY}), + XYEnvelope: NewEnvelopeXY(minX, minY, maxX, maxY), }, nil } } diff --git a/geos/entrypoints_test.go b/geos/entrypoints_test.go index c12cf7b3..4b156486 100644 --- a/geos/entrypoints_test.go +++ b/geos/entrypoints_test.go @@ -983,61 +983,61 @@ func TestClipByRect(t *testing.T) { { 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}), + rect: geom.NewEnvelopeXY(0, 0, 3, 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}), + rect: geom.NewEnvelopeXY(1, 1, 3, 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}), + rect: geom.NewEnvelopeXY(5, 5, 6, 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}), + rect: geom.NewEnvelopeXY(1, 1, 3, 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}), + rect: geom.NewEnvelopeXY(1, 1, 3, 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}), + rect: geom.NewEnvelopeXY(1, 1, 3, 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}), + rect: geom.NewEnvelopeXY(0, 0, 1, 1), want: "GEOMETRYCOLLECTION EMPTY", }, { name: "u-shaped polygon clipped through both arms produces multipolygon", input: "POLYGON((0 0,4 0,4 3,3 3,3 1,1 1,1 3,0 3,0 0))", - rect: geom.NewEnvelope(geom.XY{X: 0, Y: 2}, geom.XY{X: 4, Y: 4}), + rect: geom.NewEnvelopeXY(0, 2, 4, 4), want: "MULTIPOLYGON(((0 2,0 3,1 3,1 2,0 2)),((3 2,3 3,4 3,4 2,3 2)))", }, { name: "polygon with hole inside rect", input: "POLYGON((0 0,0 6,6 6,6 0,0 0),(2 2,4 2,4 4,2 4,2 2))", - rect: geom.NewEnvelope(geom.XY{X: 1, Y: 1}, geom.XY{X: 5, Y: 5}), + rect: geom.NewEnvelopeXY(1, 1, 5, 5), want: "POLYGON((1 1,1 5,5 5,5 1,1 1),(2 2,4 2,4 4,2 4,2 2))", }, { name: "polygon with hole partially outside rect removes hole", input: "POLYGON((0 0,0 6,6 6,6 0,0 0),(1 1,3 1,3 3,1 3,1 1))", - rect: geom.NewEnvelope(geom.XY{X: 2, Y: 2}, geom.XY{X: 5, Y: 5}), + rect: geom.NewEnvelopeXY(2, 2, 5, 5), want: "POLYGON((2 3,2 5,5 5,5 2,3 2,3 3,2 3))", }, { From 66432f7b6d9370e1fc3f1c0868d4c5861929fbcc Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Thu, 13 Aug 2026 10:29:01 +1000 Subject: [PATCH 3/3] Reword the NewEnvelopeXY changelog entry "Joins the other XY constructors" could be read as joining or combining data, which is a plausible meaning in a geometry library. Say that the constructor follows the same convention instead. --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939be5c8..6cf33085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ - Add `NewEnvelopeXY` constructor, which builds an `Envelope` from variadic x and y coordinates (x1, y1, x2, y2, ..., xn, yn), where `NewEnvelope` takes - `XY` values. It joins the other `XY` constructors such as `NewPointXY` and - `NewLineStringXY`. The result is the smallest `Envelope` containing all of the - coordinates, so no arguments gives the empty envelope and a single pair gives - a point envelope. An odd number of arguments panics. + `XY` values. It follows the same convention as the other `XY` constructors, + such as `NewPointXY` and `NewLineStringXY`. The result is the smallest + `Envelope` containing all of the coordinates, so no arguments gives the empty + envelope and a single pair gives a point envelope. An odd number of arguments + panics. ## v0.59.0