tile38/internal/clip/clip.go

143 lines
3.6 KiB
Go
Raw Normal View History

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)
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
}