You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

search_geopolygon.go 3.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. // Copyright (c) 2019 Couchbase, Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package searcher
  15. import (
  16. "fmt"
  17. "github.com/blevesearch/bleve/geo"
  18. "github.com/blevesearch/bleve/index"
  19. "github.com/blevesearch/bleve/numeric"
  20. "github.com/blevesearch/bleve/search"
  21. "math"
  22. )
  23. func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
  24. polygon []geo.Point, field string, boost float64,
  25. options search.SearcherOptions) (search.Searcher, error) {
  26. if len(polygon) < 3 {
  27. return nil, fmt.Errorf("Too few points specified for the polygon boundary")
  28. }
  29. // compute the bounding box enclosing the polygon
  30. topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err :=
  31. geo.BoundingRectangleForPolygon(polygon)
  32. if err != nil {
  33. return nil, err
  34. }
  35. // build a searcher for the bounding box on the polygon
  36. boxSearcher, err := boxSearcher(indexReader,
  37. topLeftLon, topLeftLat, bottomRightLon, bottomRightLat,
  38. field, boost, options, true)
  39. if err != nil {
  40. return nil, err
  41. }
  42. dvReader, err := indexReader.DocValueReader([]string{field})
  43. if err != nil {
  44. return nil, err
  45. }
  46. // wrap it in a filtering searcher that checks for the polygon inclusivity
  47. return NewFilteringSearcher(boxSearcher,
  48. buildPolygonFilter(dvReader, field, polygon)), nil
  49. }
  50. const float64EqualityThreshold = 1e-6
  51. func almostEqual(a, b float64) bool {
  52. return math.Abs(a-b) <= float64EqualityThreshold
  53. }
  54. // buildPolygonFilter returns true if the point lies inside the
  55. // polygon. It is based on the ray-casting technique as referred
  56. // here: https://wrf.ecse.rpi.edu/nikola/pubdetails/pnpoly.html
  57. func buildPolygonFilter(dvReader index.DocValueReader, field string,
  58. polygon []geo.Point) FilterFunc {
  59. return func(d *search.DocumentMatch) bool {
  60. // check geo matches against all numeric type terms indexed
  61. var lons, lats []float64
  62. var found bool
  63. err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) {
  64. // only consider the values which are shifted 0
  65. prefixCoded := numeric.PrefixCoded(term)
  66. shift, err := prefixCoded.Shift()
  67. if err == nil && shift == 0 {
  68. i64, err := prefixCoded.Int64()
  69. if err == nil {
  70. lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
  71. lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
  72. found = true
  73. }
  74. }
  75. })
  76. // Note: this approach works for points which are strictly inside
  77. // the polygon. ie it might fail for certain points on the polygon boundaries.
  78. if err == nil && found {
  79. nVertices := len(polygon)
  80. if len(polygon) < 3 {
  81. return false
  82. }
  83. rayIntersectsSegment := func(point, a, b geo.Point) bool {
  84. return (a.Lat > point.Lat) != (b.Lat > point.Lat) &&
  85. point.Lon < (b.Lon-a.Lon)*(point.Lat-a.Lat)/(b.Lat-a.Lat)+a.Lon
  86. }
  87. for i := range lons {
  88. pt := geo.Point{Lon: lons[i], Lat: lats[i]}
  89. inside := rayIntersectsSegment(pt, polygon[len(polygon)-1], polygon[0])
  90. // check for a direct vertex match
  91. if almostEqual(polygon[0].Lat, lats[i]) &&
  92. almostEqual(polygon[0].Lon, lons[i]) {
  93. return true
  94. }
  95. for j := 1; j < nVertices; j++ {
  96. if almostEqual(polygon[j].Lat, lats[i]) &&
  97. almostEqual(polygon[j].Lon, lons[i]) {
  98. return true
  99. }
  100. if rayIntersectsSegment(pt, polygon[j-1], polygon[j]) {
  101. inside = !inside
  102. }
  103. }
  104. if inside {
  105. return true
  106. }
  107. }
  108. }
  109. return false
  110. }
  111. }