2018-10-11 00:25:40 +03:00
|
|
|
package clip
|
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/tidwall/geojson"
|
|
|
|
"github.com/tidwall/geojson/geometry"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Clip clips the contents of a geojson object and return
|
|
|
|
func Clip(obj geojson.Object, clipper geojson.Object) (clipped geojson.Object) {
|
|
|
|
switch obj := obj.(type) {
|
|
|
|
case *geojson.Point:
|
|
|
|
return clipPoint(obj, clipper)
|
2019-02-15 01:53:46 +03:00
|
|
|
case *geojson.SimplePoint:
|
|
|
|
return clipSimplePoint(obj, clipper)
|
2018-10-11 00:25:40 +03:00
|
|
|
case *geojson.Rect:
|
|
|
|
return clipRect(obj, clipper)
|
|
|
|
case *geojson.LineString:
|
|
|
|
return clipLineString(obj, clipper)
|
|
|
|
case *geojson.Polygon:
|
|
|
|
return clipPolygon(obj, clipper)
|
|
|
|
case *geojson.Feature:
|
|
|
|
return clipFeature(obj, clipper)
|
|
|
|
case geojson.Collection:
|
|
|
|
return clipCollection(obj, clipper)
|
|
|
|
}
|
|
|
|
return obj
|
|
|
|
}
|
|
|
|
|
|
|
|
// clipSegment is Cohen-Sutherland Line Clipping
|
|
|
|
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/lineClip.html
|
|
|
|
func clipSegment(seg geometry.Segment, rect geometry.Rect) (
|
|
|
|
res geometry.Segment, rejected bool,
|
|
|
|
) {
|
|
|
|
startCode := getCode(rect, seg.A)
|
|
|
|
endCode := getCode(rect, seg.B)
|
|
|
|
if (startCode | endCode) == 0 {
|
|
|
|
// trivially accept
|
|
|
|
res = seg
|
|
|
|
} else if (startCode & endCode) != 0 {
|
|
|
|
// trivially reject
|
|
|
|
rejected = true
|
|
|
|
} else if startCode != 0 {
|
|
|
|
// start is outside. get new start.
|
|
|
|
newStart := intersect(rect, startCode, seg.A, seg.B)
|
|
|
|
res, rejected =
|
|
|
|
clipSegment(geometry.Segment{A: newStart, B: seg.B}, rect)
|
|
|
|
} else {
|
|
|
|
// end is outside. get new end.
|
|
|
|
newEnd := intersect(rect, endCode, seg.A, seg.B)
|
|
|
|
res, rejected = clipSegment(geometry.Segment{A: seg.A, B: newEnd}, rect)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// clipRing is Sutherland-Hodgman Polygon Clipping
|
|
|
|
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/intro2.html
|
|
|
|
func clipRing(ring []geometry.Point, bbox geometry.Rect) (
|
|
|
|
resRing []geometry.Point,
|
|
|
|
) {
|
|
|
|
if len(ring) < 4 {
|
|
|
|
// under 4 elements this is not a polygon ring!
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var edge uint8
|
|
|
|
var inside, prevInside bool
|
|
|
|
var prev geometry.Point
|
|
|
|
for edge = 1; edge <= 8; edge *= 2 {
|
|
|
|
prev = ring[len(ring)-2]
|
|
|
|
prevInside = (getCode(bbox, prev) & edge) == 0
|
|
|
|
for _, p := range ring {
|
|
|
|
inside = (getCode(bbox, p) & edge) == 0
|
|
|
|
if prevInside && inside {
|
|
|
|
// Staying inside
|
|
|
|
resRing = append(resRing, p)
|
|
|
|
} else if prevInside && !inside {
|
|
|
|
// Leaving
|
|
|
|
resRing = append(resRing, intersect(bbox, edge, prev, p))
|
|
|
|
} else if !prevInside && inside {
|
|
|
|
// Entering
|
|
|
|
resRing = append(resRing, intersect(bbox, edge, prev, p))
|
|
|
|
resRing = append(resRing, p)
|
|
|
|
} else {
|
|
|
|
// Staying outside
|
|
|
|
}
|
|
|
|
prev, prevInside = p, inside
|
|
|
|
}
|
|
|
|
if len(resRing) > 0 && resRing[0] != resRing[len(resRing)-1] {
|
|
|
|
resRing = append(resRing, resRing[0])
|
|
|
|
}
|
|
|
|
ring, resRing = resRing, []geometry.Point{}
|
|
|
|
if len(ring) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resRing = ring
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCode(bbox geometry.Rect, point geometry.Point) (code uint8) {
|
|
|
|
code = 0
|
|
|
|
|
|
|
|
if point.X < bbox.Min.X {
|
|
|
|
code |= 1 // left
|
|
|
|
} else if point.X > bbox.Max.X {
|
|
|
|
code |= 2 // right
|
|
|
|
}
|
|
|
|
|
|
|
|
if point.Y < bbox.Min.Y {
|
|
|
|
code |= 4 // bottom
|
|
|
|
} else if point.Y > bbox.Max.Y {
|
|
|
|
code |= 8 // top
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func intersect(bbox geometry.Rect, code uint8, start, end geometry.Point) (
|
|
|
|
new geometry.Point,
|
|
|
|
) {
|
|
|
|
if (code & 8) != 0 { // top
|
|
|
|
new = geometry.Point{
|
|
|
|
X: start.X + (end.X-start.X)*(bbox.Max.Y-start.Y)/(end.Y-start.Y),
|
|
|
|
Y: bbox.Max.Y,
|
|
|
|
}
|
|
|
|
} else if (code & 4) != 0 { // bottom
|
|
|
|
new = geometry.Point{
|
|
|
|
X: start.X + (end.X-start.X)*(bbox.Min.Y-start.Y)/(end.Y-start.Y),
|
|
|
|
Y: bbox.Min.Y,
|
|
|
|
}
|
|
|
|
} else if (code & 2) != 0 { //right
|
|
|
|
new = geometry.Point{
|
|
|
|
X: bbox.Max.X,
|
|
|
|
Y: start.Y + (end.Y-start.Y)*(bbox.Max.X-start.X)/(end.X-start.X),
|
|
|
|
}
|
|
|
|
} else if (code & 1) != 0 { // left
|
|
|
|
new = geometry.Point{
|
|
|
|
X: bbox.Min.X,
|
|
|
|
Y: start.Y + (end.Y-start.Y)*(bbox.Min.X-start.X)/(end.X-start.X),
|
|
|
|
}
|
|
|
|
} else { // should not call intersect with the zero code
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|