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

## Unreleased

- Add `NewEnvelopeXY` constructor for building an `Envelope` from variadic x/y
coordinate pairs, following the existing `New*XY` constructor pattern.

## v0.59.0

2026-03-27
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# CLAUDE.md

Update CHANGELOG.md whenever making a change visible to users of this module.
185 changes: 95 additions & 90 deletions geom/accessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,170 +4,175 @@ import (
"testing"

"github.com/peterstace/simplefeatures/geom"
"github.com/peterstace/simplefeatures/internal/test"
)

func TestPointAccessorNonEmpty(t *testing.T) {
xy, ok := geomFromWKT(t, "POINT(1 2)").MustAsPoint().XY()
expectBoolEq(t, ok, true)
expectXYEq(t, xy, geom.XY{1, 2})
xy, ok := test.FromWKT(t, "POINT(1 2)").MustAsPoint().XY()
test.Eq(t, ok, true)
test.Eq(t, xy, geom.XY{1, 2})
}

func TestPointAccessorEmpty(t *testing.T) {
_, ok := geomFromWKT(t, "POINT EMPTY").MustAsPoint().XY()
expectBoolEq(t, ok, false)
_, ok := test.FromWKT(t, "POINT EMPTY").MustAsPoint().XY()
test.Eq(t, ok, false)
}

func xyCoords(x, y float64) geom.Coordinates {
return geom.Coordinates{XY: geom.XY{x, y}, Type: geom.DimXY}
}

func TestLineStringAccessor(t *testing.T) {
ls := geomFromWKT(t, "LINESTRING(1 2,3 4,5 6)").MustAsLineString()
ls := test.FromWKT(t, "LINESTRING(1 2,3 4,5 6)").MustAsLineString()
seq := ls.Coordinates()
pt12 := xyCoords(1, 2)
pt34 := xyCoords(3, 4)
pt56 := xyCoords(5, 6)

t.Run("start", func(t *testing.T) {
want := geom.NewPoint(pt12)
expectGeomEq(t, ls.StartPoint().AsGeometry(), want.AsGeometry())
test.ExactEquals(t, ls.StartPoint().AsGeometry(), want.AsGeometry())
})
t.Run("end", func(t *testing.T) {
want := geom.NewPoint(pt56)
expectGeomEq(t, ls.EndPoint().AsGeometry(), want.AsGeometry())
test.ExactEquals(t, ls.EndPoint().AsGeometry(), want.AsGeometry())
})
t.Run("num points", func(t *testing.T) {
expectIntEq(t, seq.Length(), 3)
test.Eq(t, seq.Length(), 3)
})
t.Run("point n", func(t *testing.T) {
expectPanics(t, func() { seq.Get(-1) })
expectCoordsEq(t, seq.Get(0), pt12)
expectCoordsEq(t, seq.Get(1), pt34)
expectCoordsEq(t, seq.Get(2), pt56)
expectPanics(t, func() { seq.Get(3) })
test.Panics(t, func() { seq.Get(-1) })
test.Eq(t, seq.Get(0), pt12)
test.Eq(t, seq.Get(1), pt34)
test.Eq(t, seq.Get(2), pt56)
test.Panics(t, func() { seq.Get(3) })
})
}

func TestLineStringEmptyAccessor(t *testing.T) {
ls := geomFromWKT(t, "LINESTRING EMPTY").MustAsLineString()
ls := test.FromWKT(t, "LINESTRING EMPTY").MustAsLineString()
seq := ls.Coordinates()
emptyPoint := geomFromWKT(t, "POINT EMPTY")
emptyPoint := test.FromWKT(t, "POINT EMPTY")

t.Run("start", func(t *testing.T) {
expectGeomEq(t, ls.StartPoint().AsGeometry(), emptyPoint)
test.ExactEquals(t, ls.StartPoint().AsGeometry(), emptyPoint)
})
t.Run("end", func(t *testing.T) {
expectGeomEq(t, ls.EndPoint().AsGeometry(), emptyPoint)
test.ExactEquals(t, ls.EndPoint().AsGeometry(), emptyPoint)
})
t.Run("num points", func(t *testing.T) {
expectIntEq(t, seq.Length(), 0)
test.Eq(t, seq.Length(), 0)
})
t.Run("point n", func(t *testing.T) {
expectPanics(t, func() { seq.Get(-1) })
expectPanics(t, func() { seq.Get(0) })
expectPanics(t, func() { seq.Get(1) })
test.Panics(t, func() { seq.Get(-1) })
test.Panics(t, func() { seq.Get(0) })
test.Panics(t, func() { seq.Get(1) })
})
}

func TestLineStringAccessorWithDuplicates(t *testing.T) {
ls := geomFromWKT(t, "LINESTRING(1 2,3 4,3 4,5 6)").MustAsLineString()
ls := test.FromWKT(t, "LINESTRING(1 2,3 4,3 4,5 6)").MustAsLineString()
seq := ls.Coordinates()
pt12 := xyCoords(1, 2)
pt34 := xyCoords(3, 4)
pt56 := xyCoords(5, 6)

t.Run("num points", func(t *testing.T) {
expectIntEq(t, seq.Length(), 4)
test.Eq(t, seq.Length(), 4)
})
t.Run("point n", func(t *testing.T) {
expectPanics(t, func() { seq.Get(-1) })
expectCoordsEq(t, seq.Get(0), pt12)
expectCoordsEq(t, seq.Get(1), pt34)
expectCoordsEq(t, seq.Get(2), pt34)
expectCoordsEq(t, seq.Get(3), pt56)
expectPanics(t, func() { seq.Get(4) })
test.Panics(t, func() { seq.Get(-1) })
test.Eq(t, seq.Get(0), pt12)
test.Eq(t, seq.Get(1), pt34)
test.Eq(t, seq.Get(2), pt34)
test.Eq(t, seq.Get(3), pt56)
test.Panics(t, func() { seq.Get(4) })
})
}

func TestLineStringAccessorWithMoreDuplicates(t *testing.T) {
ls := geomFromWKT(t, "LINESTRING(1 2,1 2,3 4,3 4,3 4,5 6,5 6)").MustAsLineString()
ls := test.FromWKT(t, "LINESTRING(1 2,1 2,3 4,3 4,3 4,5 6,5 6)").MustAsLineString()
seq := ls.Coordinates()
pt12 := xyCoords(1, 2)
pt34 := xyCoords(3, 4)
pt56 := xyCoords(5, 6)

t.Run("num points", func(t *testing.T) {
expectIntEq(t, seq.Length(), 7)
test.Eq(t, seq.Length(), 7)
})
t.Run("point n", func(t *testing.T) {
expectPanics(t, func() { seq.Get(-1) })
expectCoordsEq(t, seq.Get(0), pt12)
expectCoordsEq(t, seq.Get(1), pt12)
expectCoordsEq(t, seq.Get(2), pt34)
expectCoordsEq(t, seq.Get(3), pt34)
expectCoordsEq(t, seq.Get(4), pt34)
expectCoordsEq(t, seq.Get(5), pt56)
expectCoordsEq(t, seq.Get(6), pt56)
expectPanics(t, func() { seq.Get(7) })
test.Panics(t, func() { seq.Get(-1) })
test.Eq(t, seq.Get(0), pt12)
test.Eq(t, seq.Get(1), pt12)
test.Eq(t, seq.Get(2), pt34)
test.Eq(t, seq.Get(3), pt34)
test.Eq(t, seq.Get(4), pt34)
test.Eq(t, seq.Get(5), pt56)
test.Eq(t, seq.Get(6), pt56)
test.Panics(t, func() { seq.Get(7) })
})
}

func TestPolygonAccessor(t *testing.T) {
poly := geomFromWKT(t, "POLYGON((0 0,5 0,5 3,0 3,0 0),(1 1,2 1,2 2,1 2,1 1),(3 1,4 1,4 2,3 2,3 1))").MustAsPolygon()
outer := geomFromWKT(t, "LINESTRING(0 0,5 0,5 3,0 3,0 0)")
inner0 := geomFromWKT(t, "LINESTRING(1 1,2 1,2 2,1 2,1 1)")
inner1 := geomFromWKT(t, "LINESTRING(3 1,4 1,4 2,3 2,3 1)")

expectGeomEq(t, poly.ExteriorRing().AsGeometry(), outer)
expectIntEq(t, poly.NumInteriorRings(), 2)
expectPanics(t, func() { poly.InteriorRingN(-1) })
expectGeomEq(t, poly.InteriorRingN(0).AsGeometry(), inner0)
expectGeomEq(t, poly.InteriorRingN(1).AsGeometry(), inner1)
expectPanics(t, func() { poly.InteriorRingN(2) })
poly := test.FromWKT(t, "POLYGON((0 0,5 0,5 3,0 3,0 0),(1 1,2 1,2 2,1 2,1 1),(3 1,4 1,4 2,3 2,3 1))").MustAsPolygon()
outer := test.FromWKT(t, "LINESTRING(0 0,5 0,5 3,0 3,0 0)")
inner0 := test.FromWKT(t, "LINESTRING(1 1,2 1,2 2,1 2,1 1)")
inner1 := test.FromWKT(t, "LINESTRING(3 1,4 1,4 2,3 2,3 1)")

test.ExactEquals(t, poly.ExteriorRing().AsGeometry(), outer)
test.Eq(t, poly.NumInteriorRings(), 2)
test.Panics(t, func() { poly.InteriorRingN(-1) })
test.ExactEquals(t, poly.InteriorRingN(0).AsGeometry(), inner0)
test.ExactEquals(t, poly.InteriorRingN(1).AsGeometry(), inner1)
test.Panics(t, func() { poly.InteriorRingN(2) })
}

func TestMultiPointAccessor(t *testing.T) {
mp := geomFromWKT(t, "MULTIPOINT((4 5),(2 3),(8 7))").MustAsMultiPoint()
pt0 := geomFromWKT(t, "POINT(4 5)")
pt1 := geomFromWKT(t, "POINT(2 3)")
pt2 := geomFromWKT(t, "POINT(8 7)")

expectIntEq(t, mp.NumPoints(), 3)
expectPanics(t, func() { mp.PointN(-1) })
expectGeomEq(t, mp.PointN(0).AsGeometry(), pt0)
expectGeomEq(t, mp.PointN(1).AsGeometry(), pt1)
expectGeomEq(t, mp.PointN(2).AsGeometry(), pt2)
expectPanics(t, func() { mp.PointN(3) })
mp := test.FromWKT(t, "MULTIPOINT((4 5),(2 3),(8 7))").MustAsMultiPoint()
pt0 := test.FromWKT(t, "POINT(4 5)")
pt1 := test.FromWKT(t, "POINT(2 3)")
pt2 := test.FromWKT(t, "POINT(8 7)")

test.Eq(t, mp.NumPoints(), 3)
test.Panics(t, func() { mp.PointN(-1) })
test.ExactEquals(t, mp.PointN(0).AsGeometry(), pt0)
test.ExactEquals(t, mp.PointN(1).AsGeometry(), pt1)
test.ExactEquals(t, mp.PointN(2).AsGeometry(), pt2)
test.Panics(t, func() { mp.PointN(3) })
}

func TestMultiLineStringAccessors(t *testing.T) {
mls := geomFromWKT(t, "MULTILINESTRING((1 2,3 4,5 6),(7 8,9 10,11 12))").MustAsMultiLineString()
ls0 := geomFromWKT(t, "LINESTRING(1 2,3 4,5 6)")
ls1 := geomFromWKT(t, "LINESTRING(7 8,9 10,11 12)")

expectIntEq(t, mls.NumLineStrings(), 2)
expectPanics(t, func() { mls.LineStringN(-1) })
expectGeomEq(t, mls.LineStringN(0).AsGeometry(), ls0)
expectGeomEq(t, mls.LineStringN(1).AsGeometry(), ls1)
expectPanics(t, func() { mls.LineStringN(2) })
mls := test.FromWKT(t, "MULTILINESTRING((1 2,3 4,5 6),(7 8,9 10,11 12))").MustAsMultiLineString()
ls0 := test.FromWKT(t, "LINESTRING(1 2,3 4,5 6)")
ls1 := test.FromWKT(t, "LINESTRING(7 8,9 10,11 12)")

test.Eq(t, mls.NumLineStrings(), 2)
test.Panics(t, func() { mls.LineStringN(-1) })
test.ExactEquals(t, mls.LineStringN(0).AsGeometry(), ls0)
test.ExactEquals(t, mls.LineStringN(1).AsGeometry(), ls1)
test.Panics(t, func() { mls.LineStringN(2) })
}

func TestMultiPolygonAccessors(t *testing.T) {
polys := geomFromWKT(t, "MULTIPOLYGON(((0 0,0 1,1 0,0 0)),((2 0,2 1,3 0,2 0)))").MustAsMultiPolygon()
poly0 := geomFromWKT(t, "POLYGON((0 0,0 1,1 0,0 0))")
poly1 := geomFromWKT(t, "POLYGON((2 0,2 1,3 0,2 0))")

expectIntEq(t, polys.NumPolygons(), 2)
expectPanics(t, func() { polys.PolygonN(-1) })
expectGeomEq(t, polys.PolygonN(0).AsGeometry(), poly0)
expectGeomEq(t, polys.PolygonN(1).AsGeometry(), poly1)
expectPanics(t, func() { polys.PolygonN(2) })
polys := test.FromWKT(t, "MULTIPOLYGON(((0 0,0 1,1 0,0 0)),((2 0,2 1,3 0,2 0)))").MustAsMultiPolygon()
poly0 := test.FromWKT(t, "POLYGON((0 0,0 1,1 0,0 0))")
poly1 := test.FromWKT(t, "POLYGON((2 0,2 1,3 0,2 0))")

test.Eq(t, polys.NumPolygons(), 2)
test.Panics(t, func() { polys.PolygonN(-1) })
test.ExactEquals(t, polys.PolygonN(0).AsGeometry(), poly0)
test.ExactEquals(t, polys.PolygonN(1).AsGeometry(), poly1)
test.Panics(t, func() { polys.PolygonN(2) })
}

func TestGeometryCollectionAccessors(t *testing.T) {
geoms := geomFromWKT(t, "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 0,0 0)),POLYGON((2 0,2 1,3 0,2 0)))").MustAsGeometryCollection()
geom0 := geomFromWKT(t, "POLYGON((0 0,0 1,1 0,0 0))")
geom1 := geomFromWKT(t, "POLYGON((2 0,2 1,3 0,2 0))")

expectIntEq(t, geoms.NumGeometries(), 2)
expectPanics(t, func() { geoms.GeometryN(-1) })
expectGeomEq(t, geoms.GeometryN(0), geom0)
expectGeomEq(t, geoms.GeometryN(1), geom1)
expectPanics(t, func() { geoms.GeometryN(2) })
geoms := test.FromWKT(t, "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 0,0 0)),POLYGON((2 0,2 1,3 0,2 0)))").MustAsGeometryCollection()
geom0 := test.FromWKT(t, "POLYGON((0 0,0 1,1 0,0 0))")
geom1 := test.FromWKT(t, "POLYGON((2 0,2 1,3 0,2 0))")

test.Eq(t, geoms.NumGeometries(), 2)
test.Panics(t, func() { geoms.GeometryN(-1) })
test.ExactEquals(t, geoms.GeometryN(0), geom0)
test.ExactEquals(t, geoms.GeometryN(1), geom1)
test.Panics(t, func() { geoms.GeometryN(2) })
}
5 changes: 3 additions & 2 deletions geom/alg_convex_hull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/peterstace/simplefeatures/geom"
"github.com/peterstace/simplefeatures/internal/test"
)

func TestConvexHull(t *testing.T) {
Expand Down Expand Up @@ -199,8 +200,8 @@ func TestConvexHull(t *testing.T) {
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
t.Logf("input: %s", tt.input)
got := geomFromWKT(t, tt.input).ConvexHull()
expectGeomEq(t, got, geomFromWKT(t, tt.output), geom.IgnoreOrder)
got := test.FromWKT(t, tt.input).ConvexHull()
test.ExactEquals(t, got, test.FromWKT(t, tt.output), geom.IgnoreOrder)
})
}
}
11 changes: 6 additions & 5 deletions geom/alg_densify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/peterstace/simplefeatures/geom"
"github.com/peterstace/simplefeatures/internal/test"
)

func TestDensifyEmpty(t *testing.T) {
Expand All @@ -27,7 +28,7 @@ func TestDensifyEmpty(t *testing.T) {
t.Run(ct.String(), func(t *testing.T) {
input := empty.ForceCoordinatesType(ct)
got := input.Densify(1.0)
expectGeomEq(t, got, input)
test.ExactEquals(t, got, input)
})
}
})
Expand Down Expand Up @@ -67,9 +68,9 @@ func TestDensify(t *testing.T) {
{"GEOMETRYCOLLECTION(POINT(0 0),LINESTRING(0 0,1 1))", 1.0, "GEOMETRYCOLLECTION(POINT(0 0),LINESTRING(0 0,0.5 0.5,1 1))"},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
input := geomFromWKT(t, tc.input)
input := test.FromWKT(t, tc.input)
got := input.Densify(tc.maxDist)
expectGeomEqWKT(t, got, tc.want)
test.ExactEqualsWKT(t, got, tc.want)
})
}
}
Expand All @@ -87,8 +88,8 @@ func TestDensifyInvalidMaxDist(t *testing.T) {
{"MULTIPOINT((0 0))", 0},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
input := geomFromWKT(t, tc.input)
expectPanics(t, func() { input.Densify(tc.maxDist) })
input := test.FromWKT(t, tc.input)
test.Panics(t, func() { input.Densify(tc.maxDist) })
})
}
}
5 changes: 3 additions & 2 deletions geom/alg_distance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/peterstace/simplefeatures/geom"
"github.com/peterstace/simplefeatures/internal/test"
)

func TestDistance(t *testing.T) {
Expand Down Expand Up @@ -113,8 +114,8 @@ func TestDistance(t *testing.T) {
desc = "rev"
}
t.Run(desc, func(t *testing.T) {
g1 := geomFromWKT(t, tt.wkt1)
g2 := geomFromWKT(t, tt.wkt2)
g1 := test.FromWKT(t, tt.wkt1)
g2 := test.FromWKT(t, tt.wkt2)
if flip {
g1, g2 = g2, g1
}
Expand Down
Loading
Loading