Skip to content

Commit

Permalink
clean up clipping, "open" option, improved speed
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmach committed Nov 22, 2017
1 parent 67e535b commit e649380
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 23 deletions.
54 changes: 42 additions & 12 deletions clip/clip.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
package clip

import "github.com/paulmach/orb"
import (
"github.com/paulmach/orb"
)

// Code based on https://github.com/mapbox/lineclip

// line will clip a line into a set of lines
// along the bounding box boundary.
func line(box orb.Bound, in orb.LineString) orb.MultiLineString {
func line(box orb.Bound, in orb.LineString, open bool) orb.MultiLineString {
if len(in) == 0 {
return nil
}

var out orb.MultiLineString
line := 0

codeA := bitCode(box, in[0])
var codeA int
if open {
codeA = bitCodeOpen(box, in[0])
} else {
codeA = bitCode(box, in[0])
}

loopTo := len(in)
for i := 1; i < loopTo; i++ {
a := in[i-1]
b := in[i]

codeB := bitCode(box, b)
var codeB int
if open {
codeB = bitCodeOpen(box, b)
} else {
codeB = bitCode(box, b)
}
endCode := codeB

// loops through all the intersection of the line and box.
Expand Down Expand Up @@ -143,15 +156,32 @@ func ring(box orb.Bound, in orb.Ring) orb.Ring {
// bottom 0101 0100 0110
func bitCode(b orb.Bound, p orb.Point) int {
code := 0
if p[0] < b.Left() {
if p[0] < b.Min[0] {
code |= 1
} else if p[0] > b.Max[0] {
code |= 2
}

if p[1] < b.Min[1] {
code |= 4
} else if p[1] > b.Max[1] {
code |= 8
}

return code
}

func bitCodeOpen(b orb.Bound, p orb.Point) int {
code := 0
if p[0] <= b.Min[0] {
code |= 1
} else if p[0] > b.Right() {
} else if p[0] >= b.Max[0] {
code |= 2
}

if p[1] < b.Bottom() {
if p[1] <= b.Min[1] {
code |= 4
} else if p[1] > b.Top() {
} else if p[1] >= b.Max[1] {
code |= 8
}

Expand All @@ -162,16 +192,16 @@ func bitCode(b orb.Bound, p orb.Point) int {
func intersect(box orb.Bound, edge int, a, b orb.Point) orb.Point {
if edge&8 != 0 {
// top
return orb.Point{a[0] + (b[0]-a[0])*(box.Top()-a[1])/(b[1]-a[1]), box.Top()}
return orb.Point{a[0] + (b[0]-a[0])*(box.Max[1]-a[1])/(b[1]-a[1]), box.Max[1]}
} else if edge&4 != 0 {
// bottom
return orb.Point{a[0] + (b[0]-a[0])*(box.Bottom()-a[1])/(b[1]-a[1]), box.Bottom()}
return orb.Point{a[0] + (b[0]-a[0])*(box.Min[1]-a[1])/(b[1]-a[1]), box.Min[1]}
} else if edge&2 != 0 {
// right
return orb.Point{box.Right(), a[1] + (b[1]-a[1])*(box.Right()-a[0])/(b[0]-a[0])}
return orb.Point{box.Max[0], a[1] + (b[1]-a[1])*(box.Max[0]-a[0])/(b[0]-a[0])}
} else if edge&1 != 0 {
// left
return orb.Point{box.Left(), a[1] + (b[1]-a[1])*(box.Left()-a[0])/(b[0]-a[0])}
return orb.Point{box.Min[0], a[1] + (b[1]-a[1])*(box.Min[0]-a[0])/(b[0]-a[0])}
}

panic("no edge??")
Expand Down
128 changes: 124 additions & 4 deletions clip/clip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestInternalLine(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := line(tc.bound, tc.input)
result := line(tc.bound, tc.input, false)
if !reflect.DeepEqual(result, tc.output) {
t.Errorf("incorrect clip")
t.Logf("%v", result)
Expand Down Expand Up @@ -157,7 +157,65 @@ func TestInternalRing(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Ring(tc.bound, tc.input)
result := ring(tc.bound, tc.input)
if !reflect.DeepEqual(result, tc.output) {
t.Errorf("incorrect clip")
t.Logf("%v", result)
t.Logf("%v", tc.output)
}
})
}
}

func TestLineString(t *testing.T) {
cases := []struct {
name string
bound orb.Bound
input orb.LineString
output orb.MultiLineString
}{
{
name: "clips line crossign many times",
bound: orb.Bound{Min: orb.Point{0, 0}, Max: orb.Point{20, 20}},
input: orb.LineString{
{10, -10}, {10, 30}, {20, 30}, {20, -10},
},
output: orb.MultiLineString{
{{10, 0}, {10, 20}},
{{20, 20}, {20, 0}},
},
},
{
name: "touches the sides a bunch of times",
bound: orb.Bound{Min: orb.Point{1, 1}, Max: orb.Point{6, 6}},
input: orb.LineString{{2, 3}, {1, 4}, {2, 5}, {2, 6}, {3, 5}, {4, 6}, {5, 5}, {5, 7}, {0, 7}, {0, 3}, {2, 3}},
output: orb.MultiLineString{
{{2, 3}, {1, 4}, {2, 5}, {2, 6}, {3, 5}, {4, 6}, {5, 5}, {5, 6}},
{{1, 3}, {2, 3}},
},
},
{
name: "floating point example",
bound: orb.Bound{Min: orb.Point{-91.93359375, 42.29356419217009}, Max: orb.Point{-91.7578125, 42.42345651793831}},
input: orb.LineString{
{-86.66015624999999, 42.22851735620852}, {-81.474609375, 38.51378825951165},
{-85.517578125, 37.125286284966776}, {-85.8251953125, 38.95940879245423},
{-90.087890625, 39.53793974517628}, {-91.93359375, 42.32606244456202},
{-86.66015624999999, 42.22851735620852},
},
output: orb.MultiLineString{
{
{-91.91208030440808, 42.29356419217009},
{-91.93359375, 42.32606244456202},
{-91.7578125, 42.3228109416169},
},
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := LineString(tc.bound, tc.input)
if !reflect.DeepEqual(result, tc.output) {
t.Errorf("incorrect clip")
t.Logf("%v", result)
Expand All @@ -167,7 +225,69 @@ func TestInternalRing(t *testing.T) {
}
}

func TestInternalRing_CompletelyOutside(t *testing.T) {
func TestLineString_OpenBound(t *testing.T) {
cases := []struct {
name string
bound orb.Bound
input orb.LineString
output orb.MultiLineString
}{
{
name: "clips line crossign many times",
bound: orb.Bound{Min: orb.Point{0, 0}, Max: orb.Point{20, 20}},
input: orb.LineString{
{10, -10}, {10, 30}, {20, 30}, {20, -10},
},
output: orb.MultiLineString{
{{10, 0}, {10, 20}},
},
},
{
name: "touches the sides a bunch of times",
bound: orb.Bound{Min: orb.Point{1, 1}, Max: orb.Point{6, 6}},
input: orb.LineString{{2, 3}, {1, 4}, {2, 5}, {2, 6}, {3, 5}, {4, 6}, {5, 5}, {5, 7}, {0, 7}, {0, 3}, {2, 3}},
output: orb.MultiLineString{
{{2, 3}, {1, 4}},
{{1, 4}, {2, 5}, {2, 6}},
{{2, 6}, {3, 5}, {4, 6}},
{{4, 6}, {5, 5}, {5, 6}},
{{1, 3}, {2, 3}},
},
},
{
name: "floating point example",
bound: orb.Bound{Min: orb.Point{-91.93359375, 42.29356419217009}, Max: orb.Point{-91.7578125, 42.42345651793831}},
input: orb.LineString{
{-86.66015624999999, 42.22851735620852}, {-81.474609375, 38.51378825951165},
{-85.517578125, 37.125286284966776}, {-85.8251953125, 38.95940879245423},
{-90.087890625, 39.53793974517628}, {-91.93359375, 42.32606244456202},
{-86.66015624999999, 42.22851735620852},
},
output: orb.MultiLineString{
{
{-91.91208030440808, 42.29356419217009},
{-91.93359375, 42.32606244456202},
}, {
{-91.93359375, 42.32606244456202},
{-91.7578125, 42.3228109416169},
},
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := LineString(tc.bound, tc.input, OpenBound(true))
if !reflect.DeepEqual(result, tc.output) {
t.Errorf("incorrect clip")
t.Logf("%v", result)
t.Logf("%v", tc.output)
}
})
}
}

func TestRing_CompletelyOutside(t *testing.T) {
cases := []struct {
name string
bound orb.Bound
Expand Down Expand Up @@ -242,7 +362,7 @@ func TestInternalRing_CompletelyOutside(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := ring(tc.bound, tc.input)
result := Ring(tc.bound, tc.input)
if !reflect.DeepEqual(result, tc.output) {
t.Errorf("incorrect clip")
t.Logf("%v %+v", result == nil, result)
Expand Down
50 changes: 43 additions & 7 deletions clip/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Clip will clip the geometry to the bounding box using the
// correct functions for the type.
// This operation will modify the input of '2d geometry' by using as a
// This operation will modify the input of '1d or 2d geometry' by using as a
// scratch space so clone if necessary.
func Clip(b orb.Bound, g orb.Geometry) orb.Geometry {
if g == nil {
Expand All @@ -26,6 +26,10 @@ func Clip(b orb.Bound, g orb.Geometry) orb.Geometry {
return g // Intersect check above
case orb.MultiPoint:
mp := MultiPoint(b, g)
if len(mp) == 1 {
return mp[0]
}

if mp == nil {
return nil
}
Expand All @@ -37,12 +41,16 @@ func Clip(b orb.Bound, g orb.Geometry) orb.Geometry {
return mls[0]
}

if mls == nil {
if len(mls) == 0 {
return nil
}
return mls
case orb.MultiLineString:
mls := MultiLineString(b, g)
if len(mls) == 1 {
return mls[0]
}

if mls == nil {
return nil
}
Expand All @@ -64,13 +72,21 @@ func Clip(b orb.Bound, g orb.Geometry) orb.Geometry {
return p
case orb.MultiPolygon:
mp := MultiPolygon(b, g)
if len(mp) == 1 {
return mp[0]
}

if mp == nil {
return nil
}

return mp
case orb.Collection:
c := Collection(b, g)
if len(c) == 1 {
return c[0]
}

if c == nil {
return nil
}
Expand Down Expand Up @@ -101,8 +117,18 @@ func MultiPoint(b orb.Bound, mp orb.MultiPoint) orb.MultiPoint {
}

// LineString clips the linestring to the bounding box.
func LineString(b orb.Bound, ls orb.LineString) orb.MultiLineString {
result := line(b, ls)
func LineString(b orb.Bound, ls orb.LineString, opts ...Option) orb.MultiLineString {
open := false
if len(opts) > 0 {
o := &options{}
for _, opt := range opts {
opt(o)
}

open = o.openBound
}

result := line(b, ls, open)
if len(result) == 0 {
return nil
}
Expand All @@ -112,11 +138,21 @@ func LineString(b orb.Bound, ls orb.LineString) orb.MultiLineString {

// MultiLineString clips the linestrings to the bounding box
// and returns a linestring union.
func MultiLineString(b orb.Bound, mls orb.MultiLineString) orb.MultiLineString {
func MultiLineString(b orb.Bound, mls orb.MultiLineString, opts ...Option) orb.MultiLineString {
open := false
if len(opts) > 0 {
o := &options{}
for _, opt := range opts {
opt(o)
}

open = o.openBound
}

var result orb.MultiLineString
for _, ls := range mls {
r := LineString(b, ls)
if r != nil {
r := line(b, ls, open)
if len(r) != 0 {
result = append(result, r...)
}
}
Expand Down
17 changes: 17 additions & 0 deletions clip/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package clip

type options struct {
openBound bool
}

// An Option is a possible parameter to the clip operations.
type Option func(*options)

// OpenBound is an option to treat the bound as open. i.e. any lines
// along the bound sides will be removed and a point on boundary will
// cause the line to be split.
func OpenBound(yes bool) Option {
return func(o *options) {
o.openBound = yes
}
}

0 comments on commit e649380

Please sign in to comment.