diff --git a/pkg/collection/collection.go b/pkg/collection/collection.go index 8e8fb7f9..ba3c6e32 100644 --- a/pkg/collection/collection.go +++ b/pkg/collection/collection.go @@ -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 }) diff --git a/pkg/controller/scanner.go b/pkg/controller/scanner.go index 113dde12..444cac24 100644 --- a/pkg/controller/scanner.go +++ b/pkg/controller/scanner.go @@ -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 diff --git a/pkg/controller/search.go b/pkg/controller/search.go index 86b8e174..390dbd37 100644 --- a/pkg/controller/search.go +++ b/pkg/controller/search.go @@ -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 @@ -512,7 +527,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 } @@ -521,6 +537,8 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res o: o, fields: fields, noLock: true, + clip: s.clip, + clipbox: clipbox, }) }, ) diff --git a/pkg/controller/token.go b/pkg/controller/token.go index a57f8033..e3269606 100644 --- a/pkg/controller/token.go +++ b/pkg/controller/token.go @@ -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 diff --git a/pkg/core/commands.json b/pkg/core/commands.json index 9f45c9b5..10e0d957 100644 --- a/pkg/core/commands.json +++ b/pkg/core/commands.json @@ -957,6 +957,12 @@ "multiple": true, "variadic": true }, + { + "command": "CLIP", + "name": [], + "type": [], + "optional": true + }, { "command": "NOFIELDS", "name": [], diff --git a/pkg/geojson/clip.go b/pkg/geojson/clip.go new file mode 100644 index 00000000..11ebc365 --- /dev/null +++ b/pkg/geojson/clip.go @@ -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 +} diff --git a/pkg/geojson/clip_test.go b/pkg/geojson/clip_test.go new file mode 100644 index 00000000..1e1825fd --- /dev/null +++ b/pkg/geojson/clip_test.go @@ -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") + } +} diff --git a/pkg/geojson/feature.go b/pkg/geojson/feature.go index 5120f2a1..e365e127 100644 --- a/pkg/geojson/feature.go +++ b/pkg/geojson/feature.go @@ -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 +} diff --git a/pkg/geojson/featurecollection.go b/pkg/geojson/featurecollection.go index 8e5bd4da..4e92221e 100644 --- a/pkg/geojson/featurecollection.go +++ b/pkg/geojson/featurecollection.go @@ -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 +} diff --git a/pkg/geojson/geometrycollection.go b/pkg/geojson/geometrycollection.go index 516dc94b..0a3b6977 100644 --- a/pkg/geojson/geometrycollection.go +++ b/pkg/geojson/geometrycollection.go @@ -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 +} diff --git a/pkg/geojson/linestring.go b/pkg/geojson/linestring.go index e7a68fda..66620b05 100644 --- a/pkg/geojson/linestring.go +++ b/pkg/geojson/linestring.go @@ -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 +} diff --git a/pkg/geojson/multilinestring.go b/pkg/geojson/multilinestring.go index a6dd24ee..71f5837c 100644 --- a/pkg/geojson/multilinestring.go +++ b/pkg/geojson/multilinestring.go @@ -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 +} diff --git a/pkg/geojson/multipoint.go b/pkg/geojson/multipoint.go index 5bdf564a..9f8efa11 100644 --- a/pkg/geojson/multipoint.go +++ b/pkg/geojson/multipoint.go @@ -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 +} diff --git a/pkg/geojson/multipolygon.go b/pkg/geojson/multipolygon.go index 5c805a9e..7a205c33 100644 --- a/pkg/geojson/multipolygon.go +++ b/pkg/geojson/multipolygon.go @@ -215,3 +215,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 +} diff --git a/pkg/geojson/object.go b/pkg/geojson/object.go index ca1f6570..3d40d1e4 100644 --- a/pkg/geojson/object.go +++ b/pkg/geojson/object.go @@ -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) { diff --git a/pkg/geojson/point.go b/pkg/geojson/point.go index 72c3e2df..ccd6ca1b 100644 --- a/pkg/geojson/point.go +++ b/pkg/geojson/point.go @@ -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 +} diff --git a/pkg/geojson/polygon.go b/pkg/geojson/polygon.go index 7344bdb6..db28fb03 100644 --- a/pkg/geojson/polygon.go +++ b/pkg/geojson/polygon.go @@ -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 +} diff --git a/pkg/geojson/simplepoint.go b/pkg/geojson/simplepoint.go index 358c484a..35349a5f 100644 --- a/pkg/geojson/simplepoint.go +++ b/pkg/geojson/simplepoint.go @@ -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 +} diff --git a/pkg/geojson/string.go b/pkg/geojson/string.go index cf85738d..e498ff85 100644 --- a/pkg/geojson/string.go +++ b/pkg/geojson/string.go @@ -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())