From 08b6aec65096f836d3800b4c1ba5956759acab67 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Tue, 26 Jan 2021 16:54:40 +1100 Subject: [PATCH] Use a near-minimum spanning tree for ghost lines Previously, we used a spanning tree where one node was chosen arbitrarily as the root and edges created that span out to all other nodes. This causes _many_ crossover points with the remaining geometries. By using a near-minimum spanning tree, we significantly reduce the number of crossovers. --- geom/alg_binary_op.go | 9 +- geom/alg_binary_op_test.go | 19 +-- geom/dcel_ghosts.go | 194 +++++++++++++----------------- geom/dcel_ghosts_test.go | 47 ++++++++ geom/dcel_test.go | 240 ++++++++++++++++--------------------- geom/xy.go | 11 ++ 6 files changed, 259 insertions(+), 261 deletions(-) create mode 100644 geom/dcel_ghosts_test.go diff --git a/geom/alg_binary_op.go b/geom/alg_binary_op.go index da86c746..20a14965 100644 --- a/geom/alg_binary_op.go +++ b/geom/alg_binary_op.go @@ -66,6 +66,7 @@ func binaryOp(a, b Geometry, include func(uint8) bool) (Geometry, error) { if err != nil { return Geometry{}, fmt.Errorf("internal error creating overlay: %v", err) } + g, err := overlay.extractGeometry(include) if err != nil { return Geometry{}, fmt.Errorf("internal error extracting geometry: %v", err) @@ -78,10 +79,10 @@ func createOverlay(a, b Geometry) (*doublyConnectedEdgeList, error) { return nil, errors.New("GeometryCollection argument not supported") } - aGhost := connectGeometry(a) - bGhost := connectGeometry(b) - joinGhost := connectGeometries(a, b) - ghosts := mergeMultiLineStrings([]MultiLineString{aGhost, bGhost, joinGhost.AsMultiLineString()}) + var points []XY + points = appendComponentPoints(points, a) + points = appendComponentPoints(points, b) + ghosts := spanningTree(points) a, b, ghosts, err := reNodeGeometries(a, b, ghosts) if err != nil { diff --git a/geom/alg_binary_op_test.go b/geom/alg_binary_op_test.go index ef496433..e614fe96 100644 --- a/geom/alg_binary_op_test.go +++ b/geom/alg_binary_op_test.go @@ -137,11 +137,11 @@ func TestBinaryOp(t *testing.T) { */ input1: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,2 3,2 1,0 1)))", input2: "MULTIPOLYGON(((4 0,4 1,5 1,5 0,4 0)),((1 0,1 2,3 2,3 0,1 0)))", - union: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,1 3,2 3,2 2,3 2,3 1,3 0,1 0,1 1,0 1)),((4 0,4 1,5 1,5 0,4 0)))", + union: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,2 3,2 2,3 2,3 0,1 0,1 1,0 1)),((4 0,4 1,5 1,5 0,4 0)))", inter: "POLYGON((2 2,2 1,1 1,1 2,2 2))", - fwdDiff: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,1 3,2 3,2 2,1 2,1 1,0 1)))", - revDiff: "MULTIPOLYGON(((4 0,4 1,5 1,5 0,4 0)),((1 0,1 1,2 1,2 2,3 2,3 1,3 0,1 0)))", - symDiff: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,1 3,2 3,2 2,1 2,1 1,0 1)),((1 1,2 1,2 2,3 2,3 1,3 0,1 0,1 1)),((4 0,4 1,5 1,5 0,4 0)))", + fwdDiff: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,2 3,2 2,1 2,1 1,0 1)))", + revDiff: "MULTIPOLYGON(((4 0,4 1,5 1,5 0,4 0)),((1 0,1 1,2 1,2 2,3 2,3 0,1 0)))", + symDiff: "MULTIPOLYGON(((0 4,0 5,1 5,1 4,0 4)),((0 1,0 3,2 3,2 2,1 2,1 1,0 1)),((1 1,2 1,2 2,3 2,3 0,1 0,1 1)),((4 0,4 1,5 1,5 0,4 0)))", }, { /* @@ -722,11 +722,11 @@ func TestBinaryOp(t *testing.T) { { input1: "LINESTRING(0 1,1 0)", input2: "MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", - union: "MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", + union: "MULTIPOLYGON(((0 0,0 1,1 1,1 0.5,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", inter: "LINESTRING(0 1,1 0)", fwdDiff: "GEOMETRYCOLLECTION EMPTY", - revDiff: "MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", - symDiff: "MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", + revDiff: "MULTIPOLYGON(((0 0,0 1,1 1,1 0.5,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", + symDiff: "MULTIPOLYGON(((0 0,0 1,1 1,1 0.5,1 0,0 0)),((2 0,2 1,3 1,3 0,2 0)))", }, { input1: "POLYGON((1 0,0 1,1 1,1 0))", @@ -805,6 +805,11 @@ func TestBinaryOp(t *testing.T) { input2: "MULTIPOLYGON(((0 0,2 0,2 2,0 2,0 0),(0.5 0.5,1 0.5,1 1.5,0.5 1.5,0.5 0.5)))", union: "POLYGON((0 0,1 0,2 0,2 2,0 2,0 1,0 0),(0.5000000000000001 0.5,1 0.5,1 1.5,0.5 1.5,0.5000000000000001 0.5))", }, + { + input1: "MULTILINESTRING((2 0,2 1),(2 2,2 3))", + input2: "POLYGON((0 0,0 10,10 10,10 0,0 0),(1.5 1.5,8.5 1.5,8.5 8.5,1.5 8.5,1.5 1.5))", + union: "GEOMETRYCOLLECTION(POLYGON((2 0,10 0,10 10,0 10,0 0,2 0),(1.5 1.5,1.5 8.5,8.5 8.5,8.5 1.5,1.5 1.5)),LINESTRING(2 2,2 3))", + }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { g1 := geomFromWKT(t, geomCase.input1) diff --git a/geom/dcel_ghosts.go b/geom/dcel_ghosts.go index 51bb8916..0b21da86 100644 --- a/geom/dcel_ghosts.go +++ b/geom/dcel_ghosts.go @@ -1,152 +1,122 @@ package geom -import "fmt" +import ( + "fmt" -func connectGeometry(g Geometry) MultiLineString { - var ghostLSs []LineString - var seenFirst bool - var first XY - addComponent := func(pt Point) { - xy, ok := pt.XY() - if !ok { - return - } - if seenFirst { - if first != xy { - seq := NewSequence([]float64{first.X, first.Y, xy.X, xy.Y}, DimXY) - ghostLS, err := NewLineString(seq) - if err != nil { - // Can't happen, since first and pt are not the same. - panic(fmt.Sprintf("could not construct LineString: %v", err)) - } - ghostLSs = append(ghostLSs, ghostLS) - } - } else { - seenFirst = true - first = xy - } + "github.com/peterstace/simplefeatures/rtree" +) + +// spanningTree creates a near-minimum spanning tree (using the euclidean +// distance metric) over the supplied points. The tree will consist of N-1 +// lines, where N is the number of _distinct_ xys supplied. +// +// It's a 'near' minimum spanning tree rather than a spanning tree, because we +// use a simple greedy algorithm rather than a proper minimum spanning tree +// algorithm. +func spanningTree(xys []XY) MultiLineString { + if len(xys) <= 1 { + return MultiLineString{} } - switch g.Type() { - case TypePoint: - // A single Point is already trivially connected. - case TypeMultiPoint: - mp := g.AsMultiPoint() - n := mp.NumPoints() - for i := 0; i < n; i++ { - addComponent(mp.PointN(i)) - } - case TypeLineString: - // LineStrings are already connected. - case TypeMultiLineString: - mls := g.AsMultiLineString() - n := mls.NumLineStrings() - for i := 0; i < n; i++ { - ls := mls.LineStringN(i) - addComponent(ls.StartPoint()) - } - case TypePolygon: - poly := g.AsPolygon() - addComponent(poly.ExteriorRing().StartPoint()) - n := poly.NumInteriorRings() - for i := 0; i < n; i++ { - addComponent(poly.InteriorRingN(i).StartPoint()) + // Load points into r-tree. + xys = sortAndUniquifyXYs(xys) + items := make([]rtree.BulkItem, len(xys)) + for i, xy := range xys { + items[i] = rtree.BulkItem{Box: xy.box(), RecordID: i} + } + tree := rtree.BulkLoad(items) + + // The disjoint set keeps track of which points have been joined together + // so far. Two entries in dset are in the same set iff they are connected + // in the incrementally-built spanning tree. + dset := newDisjointSet(len(xys)) + lss := make([]LineString, 0, len(xys)-1) + + for i, xyi := range xys { + if i == len(xys)-1 { + // Skip the last point, since a tree is formed from N-1 edges + // rather than N edges. The last point will be included by virtue + // of being the closest to another point. + continue } - case TypeMultiPolygon: - mp := g.AsMultiPolygon() - n := mp.NumPolygons() - for i := 0; i < n; i++ { - poly := mp.PolygonN(i) - addComponent(poly.ExteriorRing().StartPoint()) - m := poly.NumInteriorRings() - for j := 0; j < m; j++ { - addComponent(poly.InteriorRingN(j).StartPoint()) + 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. + if i == j || dset.find(i) == dset.find(j) { + return nil } - } - case TypeGeometryCollection: - gc := g.AsGeometryCollection() - n := gc.NumGeometries() - for i := 0; i < n; i++ { - addComponent(pointOnGeometry(gc.GeometryN(i))) - } - default: - panic(fmt.Sprintf("unknown geometry type: %v", g.Type())) + dset.union(i, j) + xyj := xys[j] + lss = append(lss, line{xyi, xyj}.asLineString()) + return rtree.Stop + }) } - return NewMultiLineStringFromLineStrings(ghostLSs) + return NewMultiLineStringFromLineStrings(lss) } -func connectGeometries(g1, g2 Geometry) LineString { - pt1 := pointOnGeometry(g1) - pt2 := pointOnGeometry(g2) - - xy1, ok1 := pt1.XY() - xy2, ok2 := pt2.XY() - if !ok1 || !ok2 || xy1 == xy2 { - return LineString{} +func appendXYForPoint(xys []XY, pt Point) []XY { + if xy, ok := pt.XY(); ok { + xys = append(xys, xy) } + return xys +} + +func appendXYForLineString(xys []XY, ls LineString) []XY { + return appendXYForPoint(xys, ls.StartPoint()) +} - coords := []float64{xy1.X, xy1.Y, xy2.X, xy2.Y} - ls, err := NewLineString(NewSequence(coords, DimXY)) - if err != nil { - // Can't happen, since we have already checked that xy1 != xy2. - panic(fmt.Sprintf("could not create lines string: %v", err)) +func appendXYsForPolygon(xys []XY, poly Polygon) []XY { + xys = appendXYForLineString(xys, poly.ExteriorRing()) + n := poly.NumInteriorRings() + for i := 0; i < n; i++ { + xys = appendXYForLineString(xys, poly.InteriorRingN(i)) } - return ls + return xys } -func pointOnGeometry(g Geometry) Point { +func appendComponentPoints(xys []XY, g Geometry) []XY { switch g.Type() { case TypePoint: - return g.AsPoint() + return appendXYForPoint(xys, g.AsPoint()) case TypeMultiPoint: mp := g.AsMultiPoint() n := mp.NumPoints() for i := 0; i < n; i++ { - pt := mp.PointN(i) - if !pt.IsEmpty() { - return pt - } + xys = appendXYForPoint(xys, mp.PointN(i)) } - return Point{} + return xys case TypeLineString: - return g.AsLineString().StartPoint() + ls := g.AsLineString() + return appendXYForLineString(xys, ls) case TypeMultiLineString: mls := g.AsMultiLineString() n := mls.NumLineStrings() for i := 0; i < n; i++ { - pt := mls.LineStringN(i).StartPoint() - if !pt.IsEmpty() { - return pt - } + ls := mls.LineStringN(i) + xys = appendXYForLineString(xys, ls) } - return Point{} + return xys case TypePolygon: - return pointOnGeometry(g.Boundary()) + poly := g.AsPolygon() + return appendXYsForPolygon(xys, poly) case TypeMultiPolygon: - return pointOnGeometry(g.Boundary()) + mp := g.AsMultiPolygon() + n := mp.NumPolygons() + for i := 0; i < n; i++ { + poly := mp.PolygonN(i) + xys = appendXYsForPolygon(xys, poly) + } + return xys case TypeGeometryCollection: gc := g.AsGeometryCollection() n := gc.NumGeometries() for i := 0; i < n; i++ { - pt := pointOnGeometry(gc.GeometryN(i)) - if !pt.IsEmpty() { - return pt - } + xys = appendComponentPoints(xys, gc.GeometryN(i)) } - return Point{} + return xys default: panic(fmt.Sprintf("unknown geometry type: %v", g.Type())) } } - -func mergeMultiLineStrings(mlss []MultiLineString) MultiLineString { - var lss []LineString - for _, mls := range mlss { - n := mls.NumLineStrings() - for i := 0; i < n; i++ { - lss = append(lss, mls.LineStringN(i)) - } - } - return NewMultiLineStringFromLineStrings(lss) -} diff --git a/geom/dcel_ghosts_test.go b/geom/dcel_ghosts_test.go new file mode 100644 index 00000000..faed415e --- /dev/null +++ b/geom/dcel_ghosts_test.go @@ -0,0 +1,47 @@ +package geom + +import ( + "strconv" + "testing" +) + +func TestSpanningTree(t *testing.T) { + for i, tc := range []struct { + xys []XY + wantWKT string + }{ + { + xys: nil, + wantWKT: "MULTILINESTRING EMPTY", + }, + { + xys: []XY{{1, 1}}, + wantWKT: "MULTILINESTRING EMPTY", + }, + { + xys: []XY{{2, 1}, {1, 2}}, + wantWKT: "MULTILINESTRING((2 1,1 2))", + }, + { + xys: []XY{{2, 0}, {2, 2}, {0, 0}, {1.5, 1.5}}, + wantWKT: "MULTILINESTRING((0 0,2 0),(1.5 1.5,2 2),(2 0,1.5 1.5))", + }, + { + xys: []XY{{-0.5, 0.5}, {0, 0}, {0, 1}, {1, 0}}, + wantWKT: "MULTILINESTRING((-0.5 0.5,0 0),(0 0,0 1),(0 1,1 0))", + }, + } { + t.Run(strconv.Itoa(i), func(t *testing.T) { + want, err := UnmarshalWKT(tc.wantWKT) + if err != nil { + t.Fatal(err) + } + got := spanningTree(tc.xys) + if !ExactEquals(want, got.AsGeometry(), IgnoreOrder) { + t.Logf("got: %v", got.AsText()) + t.Logf("want: %v", want.AsText()) + t.Fatal("mismatch") + } + }) + } +} diff --git a/geom/dcel_test.go b/geom/dcel_test.go index c31513cf..d6e0aa59 100644 --- a/geom/dcel_test.go +++ b/geom/dcel_test.go @@ -1083,18 +1083,18 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { | f2 | | | v13---v14 - | `, - |f9 `, - v12---v19---v11 - | f4 `,f8| - | `,| + | + | + v12---------v11 f0 + | f4 | + | | | v4----v18----v3 - | | f5 | `,f7| f0 - | | | `,| - v9----v17---v10 v20 v8-----v7 - | | `, | f1 | - | f3 |f6 `,| | - o v1-----------v2---v5-----v6 + | | f5 | | + | | | | + v9----v17---v10 | v8-----v7 + `, f6 | | | f1 | + `, | f3 | | | + o `v1-----------v2---v5-----v6 */ v1 := XY{1, 0} @@ -1115,17 +1115,15 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { v16 := XY{0, 5} v17 := XY{1, 1} v18 := XY{2, 2} - v19 := XY{1, 3} - v20 := XY{3, 1} CheckDCEL(t, overlay, DCELSpec{ - NumVerts: 10, - NumEdges: 36, - NumFaces: 10, + NumVerts: 8, + NumEdges: 26, + NumFaces: 7, Vertices: []VertexSpec{ { Label: populatedMask | inputAInSet, - Vertices: []XY{v1, v20, v2, v5}, + Vertices: []XY{v1, v2, v5}, }, { Label: populatedMask | inSetMask, @@ -1133,25 +1131,15 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { }, { Label: populatedMask | inputBInSet, - Vertices: []XY{v9, v19, v12, v13}, + Vertices: []XY{v9, v12, v13}, }, }, Edges: []EdgeSpec{ - { - EdgeLabel: populatedMask, - FaceLabel: 0, - Sequence: []XY{v20, v5}, - }, { EdgeLabel: populatedMask, FaceLabel: 0, Sequence: []XY{v5, v2}, }, - { - EdgeLabel: populatedMask, - FaceLabel: 0, - Sequence: []XY{v5, v20}, - }, { EdgeLabel: populatedMask, FaceLabel: 0, @@ -1162,21 +1150,11 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { FaceLabel: 0, Sequence: []XY{v12, v13}, }, - { - EdgeLabel: populatedMask, - FaceLabel: 0, - Sequence: []XY{v13, v19}, - }, { EdgeLabel: populatedMask, FaceLabel: 0, Sequence: []XY{v13, v12}, }, - { - EdgeLabel: populatedMask, - FaceLabel: 0, - Sequence: []XY{v19, v13}, - }, { EdgeLabel: populatedMask | inputAInSet, FaceLabel: inputAPopulated | inputAInSet, @@ -1197,16 +1175,6 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { FaceLabel: inputBPopulated, Sequence: []XY{v13, v16, v15, v14, v13}, }, - { - EdgeLabel: populatedMask | inputAInSet, - FaceLabel: inputAPopulated, - Sequence: []XY{v18, v3, v20}, - }, - { - EdgeLabel: populatedMask | inputAInSet, - FaceLabel: inputAPopulated, - Sequence: []XY{v20, v2}, - }, { EdgeLabel: populatedMask | inputAInSet, FaceLabel: inputAPopulated, @@ -1227,16 +1195,6 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { FaceLabel: inputBPopulated, Sequence: []XY{v9, v12}, }, - { - EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated, - Sequence: []XY{v12, v19}, - }, - { - EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated, - Sequence: []XY{v19, v11, v18}, - }, { EdgeLabel: populatedMask | inputAInSet, FaceLabel: inputAPopulated | inputAInSet, @@ -1247,26 +1205,6 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { FaceLabel: inputAPopulated | inputAInSet, Sequence: []XY{v1, v2}, }, - { - EdgeLabel: populatedMask | inputAInSet, - FaceLabel: inputAPopulated | inputAInSet, - Sequence: []XY{v2, v20}, - }, - { - EdgeLabel: populatedMask | inputAInSet, - FaceLabel: inputAPopulated | inputAInSet, - Sequence: []XY{v20, v3, v18}, - }, - { - EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated | inputBInSet, - Sequence: []XY{v18, v11, v19}, - }, - { - EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated | inputBInSet, - Sequence: []XY{v19, v12}, - }, { EdgeLabel: populatedMask | inputBInSet, FaceLabel: inputBPopulated | inputBInSet, @@ -1298,35 +1236,46 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { Sequence: []XY{v18, v4, v17}, }, { - EdgeLabel: populatedMask | inputAInSet, + EdgeLabel: populatedMask, FaceLabel: 0, - Sequence: []XY{v20, v18}, + Sequence: []XY{v1, v9}, }, { - EdgeLabel: populatedMask | inputAInSet, + EdgeLabel: populatedMask, FaceLabel: 0, - Sequence: []XY{v18, v20}, + Sequence: []XY{v9, v1}, + }, + { + EdgeLabel: populatedMask | inputAInSet, + FaceLabel: inputAPopulated, + Sequence: []XY{v18, v3, v2}, }, + { + EdgeLabel: populatedMask | inputAInSet, + FaceLabel: inputAPopulated | inputAInSet, + Sequence: []XY{v2, v3, v18}, + }, + { EdgeLabel: populatedMask | inputBInSet, - FaceLabel: 0, - Sequence: []XY{v19, v18}, + FaceLabel: inputBPopulated | inputBInSet, + Sequence: []XY{v18, v11, v12}, }, { EdgeLabel: populatedMask | inputBInSet, - FaceLabel: 0, - Sequence: []XY{v18, v19}, + FaceLabel: inputBPopulated, + Sequence: []XY{v12, v11, v18}, }, }, Faces: []FaceSpec{ { // f0 - First: v19, + First: v12, Second: v11, Cycle: []XY{ - v19, v11, v18, v3, v20, v5, v8, v7, - v6, v5, v2, v1, v17, v9, v12, - v13, v16, v15, v14, v13, v19, + v12, v11, v18, v3, v2, v5, v8, v7, + v6, v5, v2, v1, v9, v12, + v13, v16, v15, v14, v13, v12, }, Label: inputBPopulated | inputAPopulated, }, @@ -1348,14 +1297,14 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { // f3 First: v1, Second: v2, - Cycle: []XY{v1, v2, v20, v18, v10, v17, v1}, + Cycle: []XY{v1, v2, v3, v18, v10, v17, v1}, Label: inputBPopulated | inputAPopulated | inputAInSet, }, { // f4 First: v17, Second: v4, - Cycle: []XY{v17, v4, v18, v19, v12, v9, v17}, + Cycle: []XY{v17, v4, v18, v11, v12, v9, v17}, Label: inputBPopulated | inputAPopulated | inputBInSet, }, { @@ -1367,30 +1316,9 @@ func TestGraphOverlayReproduceHorizontalHoleLinkageBug(t *testing.T) { }, { // f6 - First: v2, - Second: v5, - Cycle: []XY{v2, v5, v20, v2}, - Label: populatedMask, - }, - { - // f7 - First: v20, - Second: v3, - Cycle: []XY{v20, v3, v18, v20}, - Label: inputBPopulated | inputAPopulated | inputAInSet, - }, - { - // f8 - First: v18, - Second: v11, - Cycle: []XY{v18, v11, v19, v18}, - Label: inputBPopulated | inputAPopulated | inputBInSet, - }, - { - // f9 - First: v12, - Second: v19, - Cycle: []XY{v12, v19, v13, v12}, + First: v1, + Second: v17, + Cycle: []XY{v1, v17, v9, v1}, Label: populatedMask, }, }, @@ -1760,9 +1688,13 @@ func TestGraphOverlayReproduceFaceAllocationBug(t *testing.T) { /* v3------v2 v7------v6 |`, f2 | | | - | `, | f0 | f3 | - | f1 `,| | | - v0------v1----v4------v5 + |\ `, | f0 | f4 | + | \ `,| | | + | \f1 v8 | | + | \ | `, | | + | f3 \ | `, | | + | \| `| | + v0------v1 v4------v5 */ v0 := XY{0, 0} @@ -1773,18 +1705,19 @@ func TestGraphOverlayReproduceFaceAllocationBug(t *testing.T) { v5 := XY{3, 0} v6 := XY{3, 1} v7 := XY{2, 1} + v8 := XY{1, 0.5} CheckDCEL(t, overlay, DCELSpec{ - NumVerts: 4, - NumEdges: 12, - NumFaces: 4, + NumVerts: 5, + NumEdges: 16, + NumFaces: 5, Vertices: []VertexSpec{ { Vertices: []XY{v1, v3}, Label: populatedMask | inSetMask, }, { - Vertices: []XY{v0, v4}, + Vertices: []XY{v0, v4, v8}, Label: populatedMask | inputBInSet, }, }, @@ -1805,9 +1738,9 @@ func TestGraphOverlayReproduceFaceAllocationBug(t *testing.T) { FaceLabel: inputBPopulated | inputBInSet, }, { - Sequence: []XY{v1, v2, v3}, + Sequence: []XY{v1, v0}, EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated | inputBInSet, + FaceLabel: inputBPopulated, }, { Sequence: []XY{v3, v0}, @@ -1820,60 +1753,91 @@ func TestGraphOverlayReproduceFaceAllocationBug(t *testing.T) { FaceLabel: inputBPopulated, }, { - Sequence: []XY{v3, v2, v1}, + Sequence: []XY{v4, v5, v6, v7, v4}, EdgeLabel: populatedMask | inputBInSet, - FaceLabel: inputBPopulated, + FaceLabel: inputBPopulated | inputBInSet, }, { - Sequence: []XY{v1, v0}, + Sequence: []XY{v4, v7, v6, v5, v4}, EdgeLabel: populatedMask | inputBInSet, FaceLabel: inputBPopulated, }, + { - Sequence: []XY{v4, v5, v6, v7, v4}, + Sequence: []XY{v1, v8}, EdgeLabel: populatedMask | inputBInSet, FaceLabel: inputBPopulated | inputBInSet, }, { - Sequence: []XY{v4, v7, v6, v5, v4}, + Sequence: []XY{v8, v1}, EdgeLabel: populatedMask | inputBInSet, FaceLabel: inputBPopulated, }, + { - Sequence: []XY{v4, v1}, + Sequence: []XY{v4, v8}, EdgeLabel: populatedMask, FaceLabel: 0, }, { - Sequence: []XY{v1, v4}, + Sequence: []XY{v8, v4}, EdgeLabel: populatedMask, FaceLabel: 0, }, + + { + Sequence: []XY{v3, v8}, + EdgeLabel: populatedMask | inputBInSet, + FaceLabel: 0, + }, + { + Sequence: []XY{v8, v3}, + EdgeLabel: populatedMask | inputBInSet, + FaceLabel: 0, + }, + + { + Sequence: []XY{v8, v2, v3}, + EdgeLabel: populatedMask | inputBInSet, + FaceLabel: inputBPopulated | inputBInSet, + }, + { + Sequence: []XY{v3, v2, v8}, + EdgeLabel: populatedMask | inputBInSet, + FaceLabel: inputBPopulated, + }, }, Faces: []FaceSpec{ { // f0 First: v1, Second: v0, - Cycle: []XY{v1, v0, v3, v2, v1, v4, v7, v6, v5, v4, v1}, + Cycle: []XY{v1, v0, v3, v2, v8, v4, v7, v6, v5, v4, v8, v1}, Label: populatedMask, }, { // f1 - First: v0, - Second: v1, - Cycle: []XY{v0, v1, v3, v0}, + First: v1, + Second: v8, + Cycle: []XY{v1, v8, v3, v1}, Label: populatedMask | inputBInSet, }, { // f2 - First: v1, + First: v8, Second: v2, - Cycle: []XY{v1, v2, v3, v1}, + Cycle: []XY{v8, v2, v3, v8}, Label: populatedMask | inputBInSet, }, { // f3 + First: v0, + Second: v1, + Cycle: []XY{v0, v1, v3, v0}, + Label: populatedMask | inputBInSet, + }, + { + // f4 First: v4, Second: v5, Cycle: []XY{v4, v5, v6, v7, v4}, diff --git a/geom/xy.go b/geom/xy.go index 83ebe72f..b9c1bcca 100644 --- a/geom/xy.go +++ b/geom/xy.go @@ -2,6 +2,8 @@ package geom import ( "math" + + "github.com/peterstace/simplefeatures/rtree" ) // XY represents a pair of X and Y coordinates. This can either represent a @@ -82,3 +84,12 @@ func (w XY) distanceSquaredTo(o XY) float64 { delta := o.Sub(w) return delta.Dot(delta) } + +func (w XY) box() rtree.Box { + return rtree.Box{ + MinX: w.X, + MinY: w.Y, + MaxX: w.X, + MaxY: w.Y, + } +}