Skip to content

Commit

Permalink
Merge 031c1d9 into 3c23168
Browse files Browse the repository at this point in the history
  • Loading branch information
peterstace committed Oct 7, 2021
2 parents 3c23168 + 031c1d9 commit 799e065
Show file tree
Hide file tree
Showing 25 changed files with 828 additions and 417 deletions.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## Unreleased

- **Breaking change**: The `Envelope` type can now be an empty envelope.
Previously, it was only able to represent a rectangle with some area, a
horizontal or vertical line, or a single point. Its `AsGeometry` returns
an empty `GeometryCollection` in the case where it's empty. The result of
`AsGeometry` is unchanged for non-empty envelopes.

- **Breaking change**: The `NewEnvelope` function signature has changed. It now
accepts a slice of `geom.XY` as the sole argument. The behaviour of the
function is the same as before, except that if no XY values are provided then
an empty envelope is returned without error.

- The `Envelope` type now has `IsEmpty`, `IsPoint`, `IsLine`, and
`IsRectanagle` methods. These correspond to the 4 possible envelope
categories.

- **Breaking change**: The `Envelope` type's `EnvelopeFromGeoms` method has
been removed. To replicate the behaviour of this method, users can construct
a `GeometryCollection` and call its `Envelope` method.

- **Breaking change**: The `Envelope` type's `Min`, `Max`, and `Center` methods
now return `Point`s rather than `XY`s. When the envelope is empty, `Min`,
`Max`, and `Center` return empty points.

- **Breaking change**: The `Envelope` type's `Distance` method now returns
`(float64, bool)` rather than `float64`. The returned boolean is only true if
the distance between the two envelopes is defined (i.e. when they are both
non-empty).

- **Breaking change**: The `Envelope` method on the `Geometry`,
`GeometryCollection`, `Point`, `LineString`, `Polygon`, `MultiPoint`,
`MultiLineString`, and `MultiPolygon` types now return `Envelope` instead of
`(Envelope, bool)`. The empty vs non-empty status is encoded inside the
envelope instead of via an explicit boolean.

## v0.32.0

2021-09-08
Expand Down
10 changes: 5 additions & 5 deletions geom/alg_distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func Distance(g1, g2 Geometry) (float64, bool) {
} else {
recordEnv = lns2[lnIdx].uncheckedEnvelope()
}
if recordEnv.Distance(env) > minDist {
if d, ok := recordEnv.Distance(env); ok && d > minDist {
return rtree.Stop
}

Expand All @@ -73,7 +73,7 @@ func Distance(g1, g2 Geometry) (float64, bool) {
}
for _, xy := range xys1 {
xyEnv := xy.uncheckedEnvelope()
tr.PrioritySearch(xyEnv.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(lnEnv.box(), func(recordID int) error {
tr.PrioritySearch(ln.box(), func(recordID int) error {
return searchBody(
lnEnv,
recordID,
Expand Down Expand Up @@ -136,13 +136,13 @@ func loadTree(xys []XY, lns []line) *rtree.RTree {
items := make([]rtree.BulkItem, len(xys)+len(lns))
for i, xy := range xys {
items[i] = rtree.BulkItem{
Box: xy.uncheckedEnvelope().box(),
Box: xy.box(),
RecordID: i + 1,
}
}
for i, ln := range lns {
items[i+len(xys)] = rtree.BulkItem{
Box: ln.uncheckedEnvelope().box(),
Box: ln.box(),
RecordID: -(i + 1),
}
}
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].uncheckedEnvelope().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
25 changes: 10 additions & 15 deletions geom/alg_intersects.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func hasIntersectionBetweenLines(
bulk := make([]rtree.BulkItem, len(lines1))
for i, ln := range lines1 {
bulk[i] = rtree.BulkItem{
Box: ln.uncheckedEnvelope().box(),
Box: ln.box(),
RecordID: i,
}
}
Expand All @@ -215,40 +215,31 @@ func hasIntersectionBetweenLines(
// Keep track of an envelope of all of the points that are in the
// intersection.
var env Envelope
var envPopulated bool

for _, lnA := range lines2 {
tree.RangeSearch(lnA.uncheckedEnvelope().box(), func(i int) error {
tree.RangeSearch(lnA.box(), func(i int) error {
lnB := lines1[i]
inter := lnA.intersectLine(lnB)
if inter.empty {
return nil
}

if !populateExtension {
envPopulated = true
env = inter.ptA.uncheckedEnvelope()
env = env.uncheckedExtend(inter.ptB)
return rtree.Stop
}

if inter.ptA != inter.ptB {
envPopulated = true
env = inter.ptA.uncheckedEnvelope()
env = env.uncheckedExtend(inter.ptB)
return rtree.Stop
}

// Single point intersection case from here onwards:

if !envPopulated {
envPopulated = true
env = inter.ptA.uncheckedEnvelope()
return nil
}

env = env.uncheckedExtend(inter.ptA)
if env.Min() != env.Max() {
if !env.IsPoint() {
return rtree.Stop
}
return nil
Expand All @@ -257,12 +248,16 @@ func hasIntersectionBetweenLines(

var ext mlsWithMLSIntersectsExtension
if populateExtension {
var single XY
if xy, ok := env.Min().XY(); ok {
single = xy
}
ext = mlsWithMLSIntersectsExtension{
multiplePoints: envPopulated && env.Min() != env.Max(),
singlePoint: env.Min(),
multiplePoints: !env.IsEmpty() && !env.IsPoint(),
singlePoint: single,
}
}
return envPopulated, ext
return !env.IsEmpty(), ext
}

func hasIntersectionMultiLineStringWithMultiPolygon(mls MultiLineString, mp MultiPolygon) bool {
Expand Down
15 changes: 11 additions & 4 deletions geom/alg_point_on_surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ func pointOnAreaSurface(poly Polygon) (Point, float64) {
// 5. The PointOnSurface is the midpoint of that largest portion.

// Find envelope midpoint.
env, ok := poly.Envelope()
env := poly.Envelope()
mid, ok := env.Center().XY()
if !ok {
return Point{}, 0
}
midY := env.Center().Y
midY := mid.Y

// Adjust mid-y value if a control point has the same Y.
var midYMatchesNode bool
Expand All @@ -82,9 +83,15 @@ func pointOnAreaSurface(poly Polygon) (Point, float64) {
}

// Create bisector.
envMin, minOK := env.Min().XY()
envMax, maxOK := env.Max().XY()
if !minOK || !maxOK {
return Point{}, 0
}

bisector := line{
XY{env.Min().X - 1, midY},
XY{env.Max().X + 1, midY},
XY{envMin.X - 1, midY},
XY{envMax.X + 1, midY},
}

// Find intersection points between the bisector and the polygon.
Expand Down
24 changes: 11 additions & 13 deletions geom/attr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,15 @@ func TestEnvelope(t *testing.T) {
t.Run(strconv.Itoa(i), func(t *testing.T) {
t.Log("wkt:", tt.wkt)
g := geomFromWKT(t, tt.wkt)
env, have := g.Envelope()
if !have {
t.Fatalf("expected to have envelope but didn't")
}
if env.Min() != tt.min {
t.Errorf("min: got=%v want=%v", env.Min(), tt.min)
}
if env.Max() != tt.max {
t.Errorf("max: got=%v want=%v", env.Max(), tt.max)
}
env := g.Envelope()

gotMin, ok := env.Min().XY()
expectTrue(t, ok)
expectXYEq(t, gotMin, tt.min)

gotMax, ok := env.Max().XY()
expectTrue(t, ok)
expectXYEq(t, gotMax, tt.max)
})
}
}
Expand All @@ -127,9 +126,8 @@ func TestNoEnvelope(t *testing.T) {
} {
t.Run(wkt, func(t *testing.T) {
g := geomFromWKT(t, wkt)
if _, have := g.Envelope(); have {
t.Errorf("have envelope but expected not to")
}
got := g.Envelope()
expectTrue(t, got.IsEmpty())
})
}
}
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 @@ -223,12 +223,12 @@ func reNodeLineString(ls LineString, cut cutSet, nodes nodeSet) (LineString, err
// Collect cut locations that are *interior* to ln.
eps := 0xFF * ulpSizeForLine(ln)
var xys []XY
cut.lnIndex.tree.RangeSearch(ln.uncheckedEnvelope().box(), func(i int) error {
cut.lnIndex.tree.RangeSearch(ln.box(), func(i int) error {
other := cut.lnIndex.lines[i]
xys = appendNewNodesFromLineLineIntersection(xys, ln, other, eps, nodes)
return nil
})
cut.ptIndex.tree.RangeSearch(ln.uncheckedEnvelope().box(), func(i int) error {
cut.ptIndex.tree.RangeSearch(ln.box(), func(i int) error {
other := cut.ptIndex.points[i]
xys = appendNewNodesFromLinePointIntersection(xys, ln, other, eps, nodes)
return nil
Expand Down
15 changes: 14 additions & 1 deletion geom/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package geom
import (
"fmt"
"math"

"github.com/peterstace/simplefeatures/rtree"
)

// line represents a line segment between two XY locations. It's an invariant
Expand All @@ -19,7 +21,18 @@ type line struct {
func (ln line) uncheckedEnvelope() Envelope {
ln.a.X, ln.b.X = sortFloat64Pair(ln.a.X, ln.b.X)
ln.a.Y, ln.b.Y = sortFloat64Pair(ln.a.Y, ln.b.Y)
return Envelope{ln.a, ln.b}
return newUncheckedEnvelope(ln.a, ln.b)
}

func (ln line) box() rtree.Box {
ln.a.X, ln.b.X = sortFloat64Pair(ln.a.X, ln.b.X)
ln.a.Y, ln.b.Y = sortFloat64Pair(ln.a.Y, ln.b.Y)
return rtree.Box{
MinX: ln.a.X,
MinY: ln.a.Y,
MaxX: ln.b.X,
MaxY: ln.b.Y,
}
}

func (ln line) length() float64 {
Expand Down
2 changes: 1 addition & 1 deletion geom/perf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,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, err := NewEnvelope(XY{}, XY{7, float64(sz + 1)})
outerRingEnv, err := NewEnvelope([]XY{{}, {7, float64(sz + 1)}})
expectNoErr(b, err)
outerRing := outerRingEnv.AsGeometry().AsPolygon().ExteriorRing()
var leftFloats, rightFloats []float64
Expand Down
2 changes: 1 addition & 1 deletion geom/rtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func newIndexedLines(lines []line) indexedLines {
bulk := make([]rtree.BulkItem, len(lines))
for i, ln := range lines {
bulk[i] = rtree.BulkItem{
Box: ln.uncheckedEnvelope().box(),
Box: ln.box(),
RecordID: i,
}
}
Expand Down
Loading

0 comments on commit 799e065

Please sign in to comment.