diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c49802..6cf33085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## 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 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 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 { 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))", }, {