From 67c5430b01cb4e01e997a84d980f0b888bb4f8de Mon Sep 17 00:00:00 2001 From: Ashwin Ramesh Date: Mon, 26 Dec 2016 13:31:32 +1100 Subject: [PATCH] Support polygons in near, within and contains. Fix a bug in intersects --- types/geofilter.go | 73 +++++++++++++++++++++++++++++++---------- types/geofilter_test.go | 17 ++++++++-- types/s2.go | 2 +- types/s2index_test.go | 2 +- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/types/geofilter.go b/types/geofilter.go index a53d61ade99..4b359e21f6b 100644 --- a/types/geofilter.go +++ b/types/geofilter.go @@ -150,12 +150,9 @@ func queryTokens(qt QueryType, data string, maxDistance float64) ([]string, *Geo return toks, &GeoQueryData{pt: pt, loop: l, qtype: qt}, nil case QueryTypeContains: - if l != nil { - return nil, nil, x.Errorf("Cannot use a polygon in a contains query") - } // For a contains query, we only need to look at the objects whose cover matches our // parents. So we take our parents and prefix with the coverPrefix to look in the index. - return createTokens(parents, coverPrefix), &GeoQueryData{pt: pt, qtype: qt}, nil + return createTokens(parents, coverPrefix), &GeoQueryData{pt: pt, loop: l, qtype: qt}, nil case QueryTypeNear: if l != nil { @@ -206,33 +203,68 @@ func (q GeoQueryData) MatchesFilter(g geom.T) bool { return false } +// WithinPolygon returns true if g1 is within g2 approximaltely. +// Note that this is very far from accurate within function and is +// a temporary fix. +// TODO(Ashwin): Improve this to make it more accurate. +func WithinPolygon(g1 *s2.Loop, g2 *s2.Loop) bool { + for _, point := range g1.Vertices() { + if !g2.ContainsPoint(point) { + return false + } + } + return true +} + +// TODO(Ashwin): Improve this to make it more accurate. +func WithinCapPolygon(g1 *s2.Loop, g2 *s2.Cap) bool { + for _, point := range g1.Vertices() { + if !g2.ContainsPoint(point) { + return false + } + } + return true +} + // returns true if the geometry represented by g is within the given loop or cap func (q GeoQueryData) isWithin(g geom.T) bool { x.AssertTruef(q.pt != nil || q.loop != nil || q.cap != nil, "At least a point, loop or cap should be defined.") - gpt, ok := g.(*geom.Point) - if !ok { + gpoly, ok := g.(*geom.Polygon) + if ok { // We will only consider points for within queries. - return false + if !ok { + return false + } + s2loop, err := loopFromPolygon(gpoly) + if err != nil { + return false + } + if q.loop != nil { + return WithinPolygon(s2loop, q.loop) + } + if q.cap != nil { + return WithinCapPolygon(s2loop, q.cap) + } } - s2pt := pointFromPoint(gpt) - if q.pt != nil { - return q.pt.ApproxEqual(s2pt) - } + gpt, ok := g.(*geom.Point) + if ok { + s2pt := pointFromPoint(gpt) + if q.pt != nil { + return q.pt.ApproxEqual(s2pt) + } - if q.loop != nil { - return q.loop.ContainsPoint(s2pt) + if q.loop != nil { + return q.loop.ContainsPoint(s2pt) + } + return q.cap.ContainsPoint(s2pt) } - return q.cap.ContainsPoint(s2pt) + return false } // returns true if the geometry represented by uid/attr contains the given point func (q GeoQueryData) contains(g geom.T) bool { x.AssertTruef(q.pt != nil || q.loop != nil, "At least a point or loop should be defined.") - if q.loop != nil { - // We don't support polygons containing polygons yet. - return false - } poly, ok := g.(*geom.Polygon) if !ok { @@ -244,6 +276,11 @@ func (q GeoQueryData) contains(g geom.T) bool { if err != nil { return false } + // If its a loop check if it lies within other loop. Else Check the point. + if q.loop != nil { + // We don't support polygons containing polygons yet. + return WithinPolygon(q.loop, s2loop) + } return s2loop.ContainsPoint(*q.pt) } diff --git a/types/geofilter_test.go b/types/geofilter_test.go index 31952eecb60..562a6d62cf4 100644 --- a/types/geofilter_test.go +++ b/types/geofilter_test.go @@ -79,7 +79,7 @@ func TestQueryTokensPolygon(t *testing.T) { if qt == QueryTypeWithin { require.Len(t, toks, 18) } else { - require.Len(t, toks, 65) + require.Len(t, toks, 66) } require.NotNil(t, qd) require.Equal(t, qd.qtype, qt) @@ -91,7 +91,7 @@ func TestQueryTokensPolygon(t *testing.T) { func TestQueryTokensPolygonError(t *testing.T) { data := formData(t, "testdata/zip.json") - qtypes := []QueryType{QueryTypeNear, QueryTypeContains} + qtypes := []QueryType{QueryTypeNear} for _, qt := range qtypes { _, _, err := queryTokens(qt, data, 0.0) require.Error(t, err) @@ -257,8 +257,19 @@ func TestMatchesFilterIntersectsPolygon(t *testing.T) { {{-120, 35}, {-121, 35}, {-121, 36}, {-120, 36}, {-120, 35}}, }) require.False(t, qd.MatchesFilter(poly)) -} + // These two polygons don't intersect. + polyOut := geom.NewPolygon(geom.XY).MustSetCoords([][]geom.Coord{ + {{-122.4989104270935, 37.736953437345356}, {-122.50504732131958, 37.729096212099975}, {-122.49515533447264, 37.732049133202324}, {-122.4989104270935, 37.736953437345356}}, + }) + + poly2 := geom.NewPolygon(geom.XY).MustSetCoords([][]geom.Coord{ + {{-122.5033039, 37.7334601}, {-122.503128, 37.7335189}, {-122.5031222, 37.7335205}, {-122.5030813, 37.7335868}, {-122.5031511, 37.73359}, {-122.5031933, 37.7335916}, {-122.5032228, 37.7336022}, {-122.5032697, 37.7335937}, {-122.5033194, 37.7335874}, {-122.5033723, 37.7335518}, {-122.503369, 37.7335068}, {-122.5033462, 37.7334474}, {-122.5033039, 37.7334601}}, + }) + data = formDataPolygon(t, polyOut) + _, qd, err = queryTokens(QueryTypeIntersects, data, 0.0) + require.False(t, qd.MatchesFilter(poly2)) +} func TestMatchesFilterNearPoint(t *testing.T) { p := geom.NewPoint(geom.XY).MustSetCoords(geom.Coord{-122.082506, 37.4249518}) data := formDataPoint(t, p) diff --git a/types/s2.go b/types/s2.go index 6f63db90405..abdcaaaa240 100644 --- a/types/s2.go +++ b/types/s2.go @@ -51,7 +51,7 @@ func (l loopRegion) edgesCross(c s2.Cell) bool { func (l loopRegion) edgesCrossPoints(pts []s2.Point) bool { n := len(pts) for i := 0; i < n; i++ { - crosser := s2.NewChainEdgeCrosser(pts[i], pts[(i+1)%n], pts[0]) + crosser := s2.NewChainEdgeCrosser(pts[i], pts[(i+1)%n], l.Vertex(0)) for i := 1; i <= l.NumEdges(); i++ { // add vertex 0 twice as it is a closed loop if crosser.EdgeOrVertexChainCrossing(l.Vertex(i)) { return true diff --git a/types/s2index_test.go b/types/s2index_test.go index db06be1d42a..64c585ab1ce 100644 --- a/types/s2index_test.go +++ b/types/s2index_test.go @@ -149,7 +149,7 @@ func TestKeyGeneratorPolygon(t *testing.T) { keys, err := IndexGeoTokens(g) require.NoError(t, err) - require.Len(t, keys, 65) + require.Len(t, keys, 66) } func testCover(file string, max int) {