Add CLIP subcommand to INTERSECTS

This commit is contained in:
Alex Roitman 2018-05-07 16:18:18 -07:00
parent 565f32cc5b
commit c00dcc6632
19 changed files with 380 additions and 9 deletions

View File

@ -497,9 +497,13 @@ func (c *Collection) Within(sparse uint8, obj geojson.Object, minLat, minLon, ma
}
// Intersects returns all object that are intersect an object or bounding box. Set obj to nil in order to use the bounding box.
func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon, maxLat, maxLon, lat, lon, meters, minZ, maxZ float64, iterator func(id string, obj geojson.Object, fields []float64) bool) bool {
func (c *Collection) Intersects(
sparse uint8, obj geojson.Object,
minLat, minLon, maxLat, maxLon, lat, lon, meters, minZ, maxZ float64, doClip bool,
iterator func(id string, obj geojson.Object, fields []float64, clipBox geojson.BBox) bool) bool {
var keepon = true
var bbox geojson.BBox
var clipbox, bbox geojson.BBox
center := geojson.Position{X: lon, Y: lat, Z: 0}
if obj != nil {
bbox = obj.CalculatedBBox()
@ -513,6 +517,9 @@ func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon
bbox = geojson.BBoxesFromCenter(lat, lon, meters)
} else {
bbox = geojson.BBox{Min: geojson.Position{X: minLon, Y: minLat, Z: minZ}, Max: geojson.Position{X: maxLon, Y: maxLat, Z: maxZ}}
if doClip {
clipbox = bbox
}
}
var bboxes []geojson.BBox
if sparse > 0 {
@ -531,7 +538,7 @@ func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon
if obj != nil {
keepon = c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.Intersects(obj) {
if iterator(id, o, fields) {
if iterator(id, o, fields, clipbox) {
return false
}
}
@ -540,7 +547,7 @@ func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon
} else if meters != -1 {
keepon = c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.IntersectsCircle(center, meters) {
if iterator(id, o, fields) {
if iterator(id, o, fields, clipbox) {
return false
}
}
@ -550,7 +557,7 @@ func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon
if keepon {
keepon = c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.IntersectsBBox(bbox) {
if iterator(id, o, fields) {
if iterator(id, o, fields, clipbox) {
return false
}
}
@ -566,21 +573,21 @@ func (c *Collection) Intersects(sparse uint8, obj geojson.Object, minLat, minLon
if obj != nil {
return c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.Intersects(obj) {
return iterator(id, o, fields)
return iterator(id, o, fields, clipbox)
}
return true
})
} else if meters != -1 {
return c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.IntersectsCircle(center, meters) {
return iterator(id, o, fields)
return iterator(id, o, fields, clipbox)
}
return true
})
}
return c.geoSearch(bbox, func(id string, o geojson.Object, fields []float64) bool {
if o.IntersectsBBox(bbox) {
return iterator(id, o, fields)
return iterator(id, o, fields, clipbox)
}
return true
})

View File

@ -65,6 +65,8 @@ type ScanWriterParams struct {
distance float64
noLock bool
ignoreGlobMatch bool
clip bool
clipbox geojson.BBox
}
func (c *Controller) newScanWriter(
@ -342,6 +344,9 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
if sw.output == outputCount {
return sw.count < sw.limit
}
if opts.clip {
opts.o = opts.o.Clipped(opts.clipbox)
}
switch sw.msg.OutputType {
case server.JSON:
var wr bytes.Buffer

View File

@ -100,6 +100,10 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string)
return
}
if s.clip {
err = errInvalidArgument("cannnot clip with point")
}
umeters := true
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
umeters = false
@ -134,6 +138,10 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string)
}
}
case "circle":
if s.clip {
err = errInvalidArgument("cannnot clip with circle")
}
var slat, slon, smeters string
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
err = errInvalidNumberOfArguments
@ -165,6 +173,10 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string)
return
}
case "object":
if s.clip {
err = errInvalidArgument("cannnot clip with object")
}
var obj string
if vs, obj, ok = tokenval(vs); !ok || obj == "" {
err = errInvalidNumberOfArguments
@ -258,6 +270,9 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string)
}
s.minLat, s.minLon, s.maxLat, s.maxLon = bing.TileXYToBounds(x, y, z)
case "get":
if s.clip {
err = errInvalidArgument("cannnot clip with get")
}
var key, id string
if vs, key, ok = tokenval(vs); !ok || key == "" {
err = errInvalidNumberOfArguments
@ -509,7 +524,8 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
s.minLat, s.minLon, s.maxLat, s.maxLon,
s.lat, s.lon, s.meters,
minZ, maxZ,
func(id string, o geojson.Object, fields []float64) bool {
s.clip,
func(id string, o geojson.Object, fields []float64, clipbox geojson.BBox) bool {
if c.hasExpired(s.key, id) {
return true
}
@ -518,6 +534,8 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
o: o,
fields: fields,
noLock: true,
clip: s.clip,
clipbox: clipbox,
})
},
)

View File

@ -245,6 +245,7 @@ type searchScanBaseTokens struct {
usparse bool
sparse uint8
desc bool
clip bool
}
func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vsout []resp.Value, t searchScanBaseTokens, err error) {
@ -551,6 +552,14 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
return
}
continue
} else if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "clip" {
vs = nvs
if t.clip {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.clip = true
continue
}
}
break

View File

@ -957,6 +957,12 @@
"multiple": true,
"variadic": true
},
{
"command": "CLIP",
"name": [],
"type": [],
"optional": true
},
{
"command": "NOFIELDS",
"name": [],

121
pkg/geojson/clip.go Normal file
View File

@ -0,0 +1,121 @@
package geojson
// Cohen-Sutherland Line Clipping
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/lineClip.html
func ClipSegment(start, end Position, bbox BBox) (resStart, resEnd Position, rejected bool) {
startCode := getCode(bbox, start)
endCode := getCode(bbox, end)
if (startCode | endCode) == 0 {
// trivially accept
resStart, resEnd = start, end
} else if (startCode & endCode) != 0 {
// trivially reject
rejected = true
} else if startCode != 0 {
// start is outside. get new start.
newStart := intersect(bbox, startCode, start, end)
resStart, resEnd, rejected = ClipSegment(newStart, end, bbox)
} else {
// end is outside. get new end.
newEnd := intersect(bbox, endCode, start, end)
resStart, resEnd, rejected = ClipSegment(start, newEnd, bbox)
}
return
}
// Sutherland-Hodgman Polygon Clipping
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/intro2.html
func ClipRing(ring[] Position, bbox BBox) (resRing []Position) {
if len(ring) < 4 {
// under 4 elements this is not a polygon ring!
return
}
var edge uint8
var inside, prevInside bool
var prev Position
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 resRing[0] != resRing[len(resRing)-1] {
resRing = append(resRing, resRing[0])
}
ring, resRing = resRing, []Position{}
}
resRing = ring
return
}
func getCode(bbox BBox, point Position) (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 BBox, code uint8, start, end Position) (new Position) {
if (code & 8) != 0 { // top
new = Position{
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 = Position{
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 = Position{
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 = Position{
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
}

57
pkg/geojson/clip_test.go Normal file
View File

@ -0,0 +1,57 @@
package geojson
import "testing"
func TestClipLineString(t *testing.T) {
ls, _ := fillLineString(
[]Position{
{X: 1, Y: 1},
{X: 2, Y: 2},
{X: 3, Y: 1},
}, nil, nil)
bbox := BBox{
Min: Position{X: 1.5, Y: 0.5},
Max: Position{X: 2.5, Y: 1.8},
}
clipped := ls.Clipped(bbox)
cl, ok := clipped.(MultiLineString)
if !ok {
t.Fatal("wrong type")
}
if len(cl.Coordinates) != 2 {
t.Fatal("result must have two parts in MultiString")
}
}
func TestClipPolygon(t *testing.T) {
outer := []Position{
{X: 2, Y: 2},
{X: 1, Y: 2},
{X: 1.5, Y: 1.5},
{X: 1, Y: 1},
{X: 2, Y: 1},
{X: 2, Y: 2},
}
inner := []Position{
{X: 1.9, Y: 1.9},
{X: 1.2, Y: 1.9},
{X: 1.45, Y: 1.65},
{X: 1.9, Y: 1.5},
{X: 1.9, Y: 1.9},
}
polygon, _ := fillPolygon([][]Position{outer, inner}, nil, nil)
bbox := BBox{
Min: Position{X: 1.3, Y: 1.3},
Max: Position{X: 1.4, Y: 2.15},
}
clipped := polygon.Clipped(bbox)
cp, ok := clipped.(Polygon)
if !ok {
t.Fatal("wrong type")
}
if len(cp.Coordinates) != 2 {
t.Fatal("result must have two parts in Polygon")
}
}

View File

@ -232,3 +232,14 @@ func (g Feature) IsBBoxDefined() bool {
func (g Feature) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g Feature) Clipped(bbox BBox) Object {
clippedGeometry := g.Geometry.Clipped(bbox)
res := Feature{Geometry: clippedGeometry, idprops: g.idprops}
cbbox := clippedGeometry.CalculatedBBox()
res.BBox = &cbbox
return res
}

View File

@ -248,3 +248,16 @@ func (g FeatureCollection) IsBBoxDefined() bool {
func (g FeatureCollection) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g FeatureCollection) Clipped(bbox BBox) Object {
var new_features []Object
for _, feature := range g.Features {
new_features = append(new_features, feature.Clipped(bbox))
}
fc := FeatureCollection{Features: new_features}
cbbox := fc.CalculatedBBox()
fc.BBox = &cbbox
return fc
}

View File

@ -246,3 +246,16 @@ func (g GeometryCollection) IsBBoxDefined() bool {
func (g GeometryCollection) IsGeometry() bool {
return true
}
// Clip returns the object of the same type as this object, clipped by a bbox.
func (g GeometryCollection) Clipped(bbox BBox) Object {
var new_geometries []Object
for _, geometry := range g.Geometries {
new_geometries = append(new_geometries, geometry.Clipped(bbox))
}
gc := GeometryCollection{Geometries: new_geometries}
cbbox := gc.CalculatedBBox()
gc.BBox = &cbbox
return gc
}

View File

@ -151,3 +151,31 @@ func (g LineString) IsBBoxDefined() bool {
func (g LineString) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g LineString) Clipped(bbox BBox) Object {
var new_coordinates [][]Position
var clipedStart, clippedEnd Position
var rejected bool
var line []Position
for i := 0; i < len(g.Coordinates) - 1 ; i++ {
clipedStart, clippedEnd, rejected = ClipSegment(g.Coordinates[i], g.Coordinates[i + 1], bbox)
if rejected {
continue
}
if len(line) > 0 && line[len(line) - 1] != clipedStart {
new_coordinates = append(new_coordinates, line)
line = []Position{clipedStart}
} else if len(line) == 0 {
line = append(line, clipedStart)
}
line = append(line, clippedEnd)
}
if len(line) > 0 {
new_coordinates = append(new_coordinates, line)
}
res, _ := fillMultiLineString(new_coordinates, nil, nil)
return res
}

View File

@ -209,3 +209,18 @@ func (g MultiLineString) IsBBoxDefined() bool {
func (g MultiLineString) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g MultiLineString) Clipped(bbox BBox) Object {
var new_coordinates [][]Position
for ix := range g.Coordinates {
clippedMultiLineString, _ := g.getLineString(ix).Clipped(bbox).(MultiLineString)
for _, ls := range clippedMultiLineString.Coordinates {
new_coordinates = append(new_coordinates, ls)
}
}
res, _ := fillMultiLineString(new_coordinates, nil, nil)
return res
}

View File

@ -177,3 +177,18 @@ func (g MultiPoint) IsBBoxDefined() bool {
func (g MultiPoint) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g MultiPoint) Clipped(bbox BBox) Object {
var new_coordinates []Position
for _, position := range g.Coordinates {
if poly.Point(position).InsideRect(rectBBox(bbox)) {
new_coordinates = append(new_coordinates, position)
}
}
res, _ := fillMultiPoint(new_coordinates, nil, nil)
return res
}

View File

@ -213,3 +213,18 @@ func (g MultiPolygon) IsBBoxDefined() bool {
func (g MultiPolygon) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g MultiPolygon) Clipped(bbox BBox) Object {
var new_coordinates [][][]Position
for _, polygon := range g.polygons {
clippedMultiPolygon, _ := polygon.Clipped(bbox).(MultiPolygon)
for _, pg := range clippedMultiPolygon.Coordinates {
new_coordinates = append(new_coordinates, pg)
}
}
res, _ := fillMultiPolygon(new_coordinates, nil, nil)
return res
}

View File

@ -94,6 +94,8 @@ type Object interface {
IsBBoxDefined() bool
// IsGeometry return true if the object is a geojson geometry object. false if it something else.
IsGeometry() bool
// Clip returns the object obtained by clipping this object by a bbox.
Clipped(bbox BBox) Object
}
func positionBBox(i int, bbox BBox, ps []Position) (int, BBox) {

View File

@ -145,3 +145,13 @@ func (g Point) IsBBoxDefined() bool {
func (g Point) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g Point) Clipped(bbox BBox) Object {
if g.IntersectsBBox(bbox) {
return g
}
res, _ := fillMultiPoint([]Position{}, nil, nil)
return res
}

View File

@ -209,3 +209,15 @@ func (g Polygon) IsBBoxDefined() bool {
func (g Polygon) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g Polygon) Clipped(bbox BBox) Object {
var new_coordinates [][]Position
for _, ring := range g.Coordinates {
new_coordinates = append(new_coordinates, ClipRing(ring, bbox))
}
res, _ := fillPolygon(new_coordinates, nil, nil)
return res
}

View File

@ -127,3 +127,12 @@ func (g SimplePoint) IsBBoxDefined() bool {
func (g SimplePoint) IsGeometry() bool {
return true
}
// Clip returns the object obtained by clipping this object by a bbox.
func (g SimplePoint) Clipped(bbox BBox) Object {
if g.IntersectsBBox(bbox) {
return g
}
res, _ := fillMultiPoint([]Position{}, nil, nil)
return res
}

View File

@ -73,6 +73,11 @@ func (s String) IsGeometry() bool {
return false
}
// Clip returns the object obtained by clipping this object by a bbox.
func (s String) Clipped(bbox BBox) Object {
return s
}
// Bytes is the bytes representation of the object.
func (s String) Bytes() []byte {
return []byte(s.String())