Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 1 addition & 3 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ linters-settings:
errcheck:
exclude-functions:
- io.Copy(os.Stdout)
- (*github.com/peterstace/simplefeatures/rtree.RTree).RangeSearch
Copy link
Owner Author

Choose a reason for hiding this comment

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

errcheck seems to get a bit confused when generics are introduced... I can longer specify funcs in exclude-functions section. Instead, I've opted to just use _ = ... whenever I want to ignore an error.

- (*github.com/peterstace/simplefeatures/rtree.RTree).PrioritySearch

# NOTE: every linter supported by golangci-lint is either explicitly included
# or excluded.
Expand Down Expand Up @@ -79,7 +77,6 @@ linters:
- importas
- ineffassign
- intrange
- ireturn
- loggercheck
- makezero
- mirror
Expand Down Expand Up @@ -143,6 +140,7 @@ linters:
- gomnd
- inamedparam
- interfacebloat
- ireturn
Copy link
Owner Author

Choose a reason for hiding this comment

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

I return has a false positive when using generics. Seems like not really that useful for this project, so disabled.

- lll
- maintidx
- nestif
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@
This includes function parameters, return types, struct fields, and type
assertions.

- **Breaking change:** The `rtree` package types and functions are now generic
over the record type. The `RTree` type is now `RTree[T]`, `BulkItem` is now
`BulkItem[T]`, and `BulkLoad` is now `BulkLoad[T]`. The `RecordID int` field
in `BulkItem` has been renamed to `Record T`. This allows users to store
their records directly in the tree rather than maintaining separate mappings
between integer IDs and records. Users can upgrade by adding type parameters
to their rtree usage (e.g., `RTree[int]` to maintain existing behavior with
integer IDs, or use a custom type like `RTree[MyRecord]` to store records
directly). The `RecordID` field in `BulkItem` should be renamed to `Record`,
and callback function signatures should change from `func(recordID int)` to
`func(record T)` where `T` is the type parameter.

- **Breaking change:** The minimum required Go version is now 1.18 (previously
1.17). This is required to support the `any` keyword.
1.17). This is required to support the `any` keyword and generics.

## v0.55.0

Expand Down
20 changes: 10 additions & 10 deletions geom/alg_distance.go
Copy link
Owner Author

Choose a reason for hiding this comment

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

TODOs:

  • Run benchmarks tests to check for a performance regression.
  • Update geom package to make better use of the generic RTree (we shouldn't need the separate arrays to hold the records any more!).

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func Distance(g1, g2 Geometry) (float64, bool) {
}
for _, xy := range xys1 {
xyEnv := xy.uncheckedEnvelope()
tr.PrioritySearch(xy.box(), func(recordID int) error {
_ = tr.PrioritySearch(xy.box(), func(recordID int) error {
return searchBody(
xyEnv,
recordID,
Expand All @@ -84,7 +84,7 @@ func Distance(g1, g2 Geometry) (float64, bool) {
}
for _, ln := range lns1 {
lnEnv := ln.uncheckedEnvelope()
tr.PrioritySearch(ln.box(), func(recordID int) error {
_ = tr.PrioritySearch(ln.box(), func(recordID int) error {
return searchBody(
lnEnv,
recordID,
Expand Down Expand Up @@ -132,18 +132,18 @@ func extractXYsAndLines(g Geometry) ([]XY, []line) {
// uses positive record IDs to refer to the XYs, and negative recordIDs to
// refer to the lines. Because +0 and -0 are the same, indexing is 1-based and
// recordID 0 is not used.
func loadTree(xys []XY, lns []line) *rtree.RTree {
items := make([]rtree.BulkItem, len(xys)+len(lns))
func loadTree(xys []XY, lns []line) *rtree.RTree[int] {
items := make([]rtree.BulkItem[int], len(xys)+len(lns))
for i, xy := range xys {
items[i] = rtree.BulkItem{
Box: xy.box(),
RecordID: i + 1,
items[i] = rtree.BulkItem[int]{
Box: xy.box(),
Record: i + 1,
}
}
for i, ln := range lns {
items[i+len(xys)] = rtree.BulkItem{
Box: ln.box(),
RecordID: -(i + 1),
items[i+len(xys)] = rtree.BulkItem[int]{
Box: ln.box(),
Record: -(i + 1),
}
}
return rtree.BulkLoad(items)
Expand Down
2 changes: 1 addition & 1 deletion geom/alg_intersection.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func intersectionOfIndexedLines(
var pts []Point
seen := make(map[XY]bool)
for i := range lines1.lines {
lines2.tree.RangeSearch(lines1.lines[i].box(), func(j int) error {
_ = lines2.tree.RangeSearch(lines1.lines[i].box(), func(j int) error {
inter := lines1.lines[i].intersectLine(lines2.lines[j])
if inter.empty {
return nil
Expand Down
10 changes: 5 additions & 5 deletions geom/alg_intersects.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,11 @@ func hasIntersectionBetweenLines(
lines1, lines2 = lines2, lines1
}

bulk := make([]rtree.BulkItem, len(lines1))
bulk := make([]rtree.BulkItem[int], len(lines1))
for i, ln := range lines1 {
bulk[i] = rtree.BulkItem{
Box: ln.box(),
RecordID: i,
bulk[i] = rtree.BulkItem[int]{
Box: ln.box(),
Record: i,
}
}
tree := rtree.BulkLoad(bulk)
Expand All @@ -209,7 +209,7 @@ func hasIntersectionBetweenLines(
var env Envelope

for _, lnA := range lines2 {
tree.RangeSearch(lnA.box(), func(i int) error {
_ = tree.RangeSearch(lnA.box(), func(i int) error {
lnB := lines1[i]
inter := lnA.intersectLine(lnB)
if inter.empty {
Expand Down
2 changes: 1 addition & 1 deletion geom/alg_point_in_ring.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func relatePointToPolygon(pt XY, polyBoundary indexedLines) side {
}
var onBound bool
var count int
polyBoundary.tree.RangeSearch(box, func(i int) error {
_ = polyBoundary.tree.RangeSearch(box, func(i int) error {
ln := polyBoundary.lines[i]
crossing, onLine := hasCrossing(pt, ln)
if onLine {
Expand Down
6 changes: 3 additions & 3 deletions geom/dcel_ghosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ func spanningTree(xys []XY) MultiLineString {

// Load points into r-tree.
xys = sortAndUniquifyXYs(xys)
items := make([]rtree.BulkItem, len(xys))
items := make([]rtree.BulkItem[int], len(xys))
for i, xy := range xys {
items[i] = rtree.BulkItem{Box: xy.box(), RecordID: i}
items[i] = rtree.BulkItem[int]{Box: xy.box(), Record: i}
}
tree := rtree.BulkLoad(items)

Expand All @@ -49,7 +49,7 @@ func spanningTree(xys []XY) MultiLineString {
// of being the closest to another point.
continue
}
tree.PrioritySearch(xyi.box(), func(j int) error {
_ = tree.PrioritySearch(xyi.box(), func(j int) error {
// We don't want to include a new edge in the spanning tree if it
// would cause a cycle (i.e. the two endpoints are already in the
// same tree). This is checked via dset.
Expand Down
4 changes: 2 additions & 2 deletions geom/dcel_re_noding.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func reNodeGeometries(g1, g2 Geometry, mls MultiLineString) (Geometry, Geometry,
// Create new nodes for point/line intersections.
ptIndex := newIndexedPoints(nodes.list())
appendCutsForPointXLine := func(ln line, cuts []XY) []XY {
ptIndex.tree.RangeSearch(ln.box(), func(i int) error {
_ = ptIndex.tree.RangeSearch(ln.box(), func(i int) error {
xy := ptIndex.points[i]
if !ln.hasEndpoint(xy) && distBetweenXYAndLine(xy, ln) < ulp*0x200 {
cuts = append(cuts, xy)
Expand All @@ -64,7 +64,7 @@ func reNodeGeometries(g1, g2 Geometry, mls MultiLineString) (Geometry, Geometry,
// Create new nodes for line/line intersections.
lnIndex := newIndexedLines(appendLines(nil, all()))
appendCutsLineXLine := func(ln line, cuts []XY) []XY {
lnIndex.tree.RangeSearch(ln.box(), func(i int) error {
_ = lnIndex.tree.RangeSearch(ln.box(), func(i int) error {
other := lnIndex.lines[i]

// TODO: This is a hacky approach (re-orders inputs, rather than
Expand Down
20 changes: 10 additions & 10 deletions geom/rtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import "github.com/peterstace/simplefeatures/rtree"
// the indices of the lines slice.
type indexedLines struct {
lines []line
tree *rtree.RTree
tree *rtree.RTree[int]
}

func newIndexedLines(lines []line) indexedLines {
bulk := make([]rtree.BulkItem, len(lines))
bulk := make([]rtree.BulkItem[int], len(lines))
for i, ln := range lines {
bulk[i] = rtree.BulkItem{
Box: ln.box(),
RecordID: i,
bulk[i] = rtree.BulkItem[int]{
Box: ln.box(),
Record: i,
}
}
return indexedLines{lines, rtree.BulkLoad(bulk)}
Expand All @@ -26,15 +26,15 @@ func newIndexedLines(lines []line) indexedLines {
// the indices of the points slice.
type indexedPoints struct {
points []XY
tree *rtree.RTree
tree *rtree.RTree[int]
}

func newIndexedPoints(points []XY) indexedPoints {
bulk := make([]rtree.BulkItem, len(points))
bulk := make([]rtree.BulkItem[int], len(points))
for i, pt := range points {
bulk[i] = rtree.BulkItem{
Box: rtree.Box{MinX: pt.X, MaxX: pt.X, MinY: pt.Y, MaxY: pt.Y},
RecordID: i,
bulk[i] = rtree.BulkItem[int]{
Box: rtree.Box{MinX: pt.X, MaxX: pt.X, MinY: pt.Y, MaxY: pt.Y},
Record: i,
}
}
return indexedPoints{points, rtree.BulkLoad(bulk)}
Expand Down
6 changes: 3 additions & 3 deletions geom/type_line_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,13 @@ func (s LineString) IsSimple() bool {
}

n := s.seq.Length()
items := make([]rtree.BulkItem, 0, n-1)
items := make([]rtree.BulkItem[int], 0, n-1)
for i := 0; i < n; i++ {
ln, ok := getLine(s.seq, i)
if !ok {
continue
}
items = append(items, rtree.BulkItem{Box: ln.box(), RecordID: i})
items = append(items, rtree.BulkItem[int]{Box: ln.box(), Record: i})
}
tree := rtree.BulkLoad(items)

Expand All @@ -142,7 +142,7 @@ func (s LineString) IsSimple() bool {
}

simple := true // assume simple until proven otherwise
tree.RangeSearch(ln.box(), func(j int) error {
_ = tree.RangeSearch(ln.box(), func(j int) error {
// Skip finding the original line (i == j) or cases where we have
// already checked that pair (i > j).
if i >= j {
Expand Down
10 changes: 5 additions & 5 deletions geom/type_multi_line_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (m MultiLineString) IsSimple() bool {
for _, ls := range m.lines {
numItems += maxInt(0, ls.Coordinates().Length()-1)
}
items := make([]rtree.BulkItem, 0, numItems)
items := make([]rtree.BulkItem[int], 0, numItems)
for i, ls := range m.lines {
seq := ls.Coordinates()
seqLen := seq.Length()
Expand All @@ -134,9 +134,9 @@ func (m MultiLineString) IsSimple() bool {
if !ok {
continue
}
items = append(items, rtree.BulkItem{
Box: ln.box(),
RecordID: toRecordID(i, j),
items = append(items, rtree.BulkItem[int]{
Box: ln.box(),
Record: toRecordID(i, j),
})
}
}
Expand All @@ -151,7 +151,7 @@ func (m MultiLineString) IsSimple() bool {
continue
}
isSimple := true // assume simple until proven otherwise
tree.RangeSearch(ln.box(), func(recordID int) error {
_ = tree.RangeSearch(ln.box(), func(recordID int) error {
// Ignore the intersection if it's for the same LineString that we're currently looking up.
lineStringIdx, seqIdx := fromRecordID(recordID)
if lineStringIdx == i {
Expand Down
6 changes: 3 additions & 3 deletions geom/type_multi_polygon.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ func (m MultiPolygon) checkMultiPolygonConstraints() error {

// Construct RTree of Polygons.
boxes := make([]rtree.Box, len(m.polys))
items := make([]rtree.BulkItem, 0, len(m.polys))
items := make([]rtree.BulkItem[int], 0, len(m.polys))
for i, p := range m.polys {
if box, ok := p.Envelope().AsBox(); ok {
boxes[i] = box
item := rtree.BulkItem{Box: boxes[i], RecordID: i}
item := rtree.BulkItem[int]{Box: boxes[i], Record: i}
items = append(items, item)
}
}
Expand Down Expand Up @@ -142,7 +142,7 @@ func validatePolyNotInsidePoly(p1, p2 indexedLines) error {
for j := range p2.lines {
// Find intersection points.
var pts []XY
p1.tree.RangeSearch(p2.lines[j].box(), func(i int) error {
_ = p1.tree.RangeSearch(p2.lines[j].box(), func(i int) error {
inter := p1.lines[i].intersectLine(p2.lines[j])
if inter.empty {
return nil
Expand Down
4 changes: 2 additions & 2 deletions geom/type_polygon.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (p Polygon) Validate() error {

// Construct RTree of rings.
boxes := make([]rtree.Box, len(p.rings))
items := make([]rtree.BulkItem, len(p.rings))
items := make([]rtree.BulkItem[int], len(p.rings))
for i, r := range p.rings {
box, ok := r.Envelope().AsBox()
if !ok {
Expand All @@ -74,7 +74,7 @@ func (p Polygon) Validate() error {
panic("unexpected empty ring")
}
boxes[i] = box
items[i] = rtree.BulkItem{Box: boxes[i], RecordID: i}
items[i] = rtree.BulkItem[int]{Box: boxes[i], Record: i}
}
tree := rtree.BulkLoad(items)

Expand Down
2 changes: 1 addition & 1 deletion rtree/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Box struct {
}

// calculateBound calculates the smallest bounding box that fits a node.
func calculateBound(n *node) Box {
func calculateBound[T any](n *node[T]) Box {
box := n.entries[0].box
for i := 1; i < n.numEntries; i++ {
box = combine(box, n.entries[i].box)
Expand Down
Loading