From fdbe669fd593e1e82a0a4c799000b8e081ce3bc7 Mon Sep 17 00:00:00 2001 From: Marty Schoch Date: Wed, 29 Mar 2017 14:21:59 -0400 Subject: [PATCH] several more items on the geo checklist - added readme pointing back to lucene origins - improved documentation of exported methods in geo package - improved test coverage to 100% on geo package - added support for parsing geojson style points - removed some duplicated code in the geo bounding box searcher --- geo/README.md | 9 ++ geo/geo.go | 65 ++++++-- geo/geo_dist.go | 26 +++- geo/geo_dist_test.go | 14 ++ geo/geo_test.go | 141 +++++++++++++++++ geo/parse.go | 65 +++++++- geo/parse_test.go | 184 +++++++++++++++++++++++ geo/sloppy.go | 23 ++- geo/sloppy_test.go | 19 ++- search/searcher/search_geoboundingbox.go | 58 ++----- 10 files changed, 542 insertions(+), 62 deletions(-) create mode 100644 geo/README.md create mode 100644 geo/parse_test.go diff --git a/geo/README.md b/geo/README.md new file mode 100644 index 00000000..43bcd98f --- /dev/null +++ b/geo/README.md @@ -0,0 +1,9 @@ +# geo support in bleve + +First, all of this geo code is a Go adaptation of the [Lucene 5.3.2 sandbox geo support](https://lucene.apache.org/core/5_3_2/sandbox/org/apache/lucene/util/package-summary.html). + +## Notes + +- All of the APIs will use float64 for lon/lat values. +- When describing a point in function arguments or return values, we always use the order lon, lat. +- High level APIs will use TopLeft and BottomRight to describe bounding boxes. This may not map cleanly to min/max lon/lat when crossing the dateline. The lower level APIs will use min/max lon/lat and require the higher-level code to split boxes accordingly. diff --git a/geo/geo.go b/geo/geo.go index 37ed819e..3212539b 100644 --- a/geo/geo.go +++ b/geo/geo.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( @@ -6,13 +20,18 @@ import ( "github.com/blevesearch/bleve/numeric" ) +// GeoBits is the number of bits used for a single geo point +// Currently this is 32bits for lon and 32bits for lat +var GeoBits uint = 32 + var minLon = -180.0 var minLat = -90.0 -var GeoBits uint = 32 var geoTolerance = 1E-6 var lonScale = float64((uint64(0x1)<> 1)) } @@ -43,6 +64,8 @@ func unscaleLat(lat uint64) float64 { return (float64(lat) / latScale) + minLat } +// compareGeo will compare two float values and see if they are the same +// taking into consideration a known geo tolerance. func compareGeo(a, b float64) float64 { compare := a - b if math.Abs(compare) <= geoTolerance { @@ -51,25 +74,34 @@ func compareGeo(a, b float64) float64 { return compare } +// RectIntersects checks whether rectangles a and b intersect func RectIntersects(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { return !(aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) } +// RectWithin checks whether box a is within box b func RectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { rv := !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY) return rv } +// BoundingBoxContains checks whether the lon/lat point is within the box func BoundingBoxContains(lon, lat, minLon, minLat, maxLon, maxLat float64) bool { - return compareGeo(lon, minLon) >= 0 && compareGeo(lon, maxLon) <= 0 && compareGeo(lat, minLat) >= 0 && compareGeo(lat, maxLat) <= 0 + return compareGeo(lon, minLon) >= 0 && compareGeo(lon, maxLon) <= 0 && + compareGeo(lat, minLat) >= 0 && compareGeo(lat, maxLat) <= 0 } -func ComputeBoundingBox(centerLon, centerLat, radius float64) (upperLeftLon float64, upperLeftLat float64, lowerRightLon float64, lowerRightLat float64) { +// ComputeBoundingBox will compute a bounding box around the provided point +// which surrounds a circle of the provided radius (in meters). +func ComputeBoundingBox(centerLon, centerLat, + radius float64) (upperLeftLon float64, upperLeftLat float64, + lowerRightLon float64, lowerRightLat float64) { _, tlat := pointFromLonLatBearing(centerLon, centerLat, 0, radius) rlon, _ := pointFromLonLatBearing(centerLon, centerLat, 90, radius) _, blat := pointFromLonLatBearing(centerLon, centerLat, 180, radius) llon, _ := pointFromLonLatBearing(centerLon, centerLat, 270, radius) - return normalizeLon(llon), normalizeLat(tlat), normalizeLon(rlon), normalizeLat(blat) + return normalizeLon(llon), normalizeLat(tlat), + normalizeLon(rlon), normalizeLat(blat) } const degreesToRadian = math.Pi / 180 @@ -80,15 +112,22 @@ const semiMinorAxis = semiMajorAxis * (1.0 - flattening) const semiMajorAxis2 = semiMajorAxis * semiMajorAxis const semiMinorAxis2 = semiMinorAxis * semiMinorAxis +// DegreesToRadians converts an angle in degrees to radians func DegreesToRadians(d float64) float64 { return d * degreesToRadian } +// RadiansToDegrees converts an angle in radians to degress func RadiansToDegrees(r float64) float64 { return r * radiansToDegrees } -func pointFromLonLatBearing(lon, lat, bearing, dist float64) (float64, float64) { +// pointFromLonLatBearing starts that the provide lon,lat +// then moves in the bearing direction (in degrees) +// this move continues for the provided distance (in meters) +// The lon, lat of this destination location is returned. +func pointFromLonLatBearing(lon, lat, bearing, + dist float64) (float64, float64) { alpha1 := DegreesToRadians(bearing) cosA1 := math.Cos(alpha1) @@ -108,23 +147,29 @@ func pointFromLonLatBearing(lon, lat, bearing, dist float64) (float64, float64) cos25SigmaM := math.Cos(2*sig1 + sigma) sinSigma := math.Sin(sigma) cosSigma := math.Cos(sigma) - deltaSigma := B * sinSigma * (cos25SigmaM + (B/4)*(cosSigma*(-1+2*cos25SigmaM*cos25SigmaM)-(B/6)*cos25SigmaM*(-1+4*sinSigma*sinSigma)*(-3+4*cos25SigmaM*cos25SigmaM))) + deltaSigma := B * sinSigma * (cos25SigmaM + (B/4)* + (cosSigma*(-1+2*cos25SigmaM*cos25SigmaM)-(B/6)*cos25SigmaM* + (-1+4*sinSigma*sinSigma)*(-3+4*cos25SigmaM*cos25SigmaM))) sigmaP := sigma sigma = dist/(semiMinorAxis*A) + deltaSigma for math.Abs(sigma-sigmaP) > 1E-12 { cos25SigmaM = math.Cos(2*sig1 + sigma) sinSigma = math.Sin(sigma) cosSigma = math.Cos(sigma) - deltaSigma = B * sinSigma * (cos25SigmaM + (B/4)*(cosSigma*(-1+2*cos25SigmaM*cos25SigmaM)-(B/6)*cos25SigmaM*(-1+4*sinSigma*sinSigma)*(-3+4*cos25SigmaM*cos25SigmaM))) + deltaSigma = B * sinSigma * (cos25SigmaM + (B/4)* + (cosSigma*(-1+2*cos25SigmaM*cos25SigmaM)-(B/6)*cos25SigmaM* + (-1+4*sinSigma*sinSigma)*(-3+4*cos25SigmaM*cos25SigmaM))) sigmaP = sigma sigma = dist/(semiMinorAxis*A) + deltaSigma } tmp := sinU1*sinSigma - cosU1*cosSigma*cosA1 - lat2 := math.Atan2(sinU1*cosSigma+cosU1*sinSigma*cosA1, (1-flattening)*math.Sqrt(sinAlpha*sinAlpha+tmp*tmp)) + lat2 := math.Atan2(sinU1*cosSigma+cosU1*sinSigma*cosA1, + (1-flattening)*math.Sqrt(sinAlpha*sinAlpha+tmp*tmp)) lamda := math.Atan2(sinSigma*sinA1, cosU1*cosSigma-sinU1*sinSigma*cosA1) c := flattening / 16 * cosSqAlpha * (4 + flattening*(4-3*cosSqAlpha)) - lam := lamda - (1-c)*flattening*sinAlpha*(sigma+c*sinSigma*(cos25SigmaM+c*cosSigma*(-1+2*cos25SigmaM*cos25SigmaM))) + lam := lamda - (1-c)*flattening*sinAlpha* + (sigma+c*sinSigma*(cos25SigmaM+c*cosSigma*(-1+2*cos25SigmaM*cos25SigmaM))) rvlon := lon + RadiansToDegrees(lam) rvlat := RadiansToDegrees(lat2) @@ -132,6 +177,7 @@ func pointFromLonLatBearing(lon, lat, bearing, dist float64) (float64, float64) return rvlon, rvlat } +// normalizeLon normalizes a longitude value within the -180 to 180 range func normalizeLon(lonDeg float64) float64 { if lonDeg >= -180 && lonDeg <= 180 { return lonDeg @@ -146,6 +192,7 @@ func normalizeLon(lonDeg float64) float64 { return -180 + off } +// normalizeLat normalizes a latitude value within the -90 to 90 range func normalizeLat(latDeg float64) float64 { if latDeg >= -90 && latDeg <= 90 { return latDeg diff --git a/geo/geo_dist.go b/geo/geo_dist.go index d9da1bde..859e6a7b 100644 --- a/geo/geo_dist.go +++ b/geo/geo_dist.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( @@ -25,7 +39,14 @@ var distanceUnits = []*distanceUnit{ &inch, &yard, &feet, &kilom, &nauticalm, &millim, ¢im, &miles, &meters, } -// ParseDistance attempts to parse a distance, return distance in meters +// ParseDistance attempts to parse a distance string and return distance in +// meters. Example formats supported: +// "5in" "5inch" "7yd" "7yards" "9ft" "9feet" "11km" "11kilometers" +// "3nm" "3nauticalmiles" "13mm" "13millimeters" "15cm" "15centimeters" +// "17mi" "17miles" "19m" "19meters" +// If the unit cannot be determined, the entire string is parsed and the +// unit of meters is assumed. +// If the number portion cannot be parsed, 0 and the parse error are returned. func ParseDistance(d string) (float64, error) { for _, unit := range distanceUnits { for _, unitSuffix := range unit.suffixes { @@ -46,6 +67,9 @@ func ParseDistance(d string) (float64, error) { return parsedNum, nil } +// Haversin computes the distance between two points. +// This implemenation uses the sloppy math implemenations which trade off +// accuracy for performance. func Haversin(lon1, lat1, lon2, lat2 float64) float64 { x1 := lat1 * degreesToRadian x2 := lat2 * degreesToRadian diff --git a/geo/geo_dist_test.go b/geo/geo_dist_test.go index 2ed57ac1..8067da8a 100644 --- a/geo/geo_dist_test.go +++ b/geo/geo_dist_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( diff --git a/geo/geo_test.go b/geo/geo_test.go index 2981dedb..c37df4c8 100644 --- a/geo/geo_test.go +++ b/geo/geo_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( @@ -79,3 +93,130 @@ func TestComputeBoundingBoxCheckLatitudeAtEquator(t *testing.T) { t.Errorf("expected bounding box lower right lat to be almost -1, got %f", lowerRightLat) } } + +func TestRectIntersects(t *testing.T) { + tests := []struct { + aMinX float64 + aMinY float64 + aMaxX float64 + aMaxY float64 + bMinX float64 + bMinY float64 + bMaxX float64 + bMaxY float64 + want bool + }{ + // clearly overlap + {0, 0, 2, 2, 1, 1, 3, 3, true}, + // clearly do not overalp + {0, 0, 1, 1, 2, 2, 3, 3, false}, + // share common point + {0, 0, 1, 1, 1, 1, 2, 2, true}, + } + + for _, test := range tests { + got := RectIntersects(test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + if test.want != got { + t.Errorf("expected intersects %t, got %t for %f %f %f %f %f %f %f %f", test.want, got, test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + } + } +} + +func TestRectWithin(t *testing.T) { + tests := []struct { + aMinX float64 + aMinY float64 + aMaxX float64 + aMaxY float64 + bMinX float64 + bMinY float64 + bMaxX float64 + bMaxY float64 + want bool + }{ + // clearly within + {1, 1, 2, 2, 0, 0, 3, 3, true}, + // clearly not within + {0, 0, 1, 1, 2, 2, 3, 3, false}, + // overlapping + {0, 0, 2, 2, 1, 1, 3, 3, false}, + // share common point + {0, 0, 1, 1, 1, 1, 2, 2, false}, + // within, but boxes reversed (b is within a, but not a within b) + {0, 0, 3, 3, 1, 1, 2, 2, false}, + } + + for _, test := range tests { + got := RectWithin(test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + if test.want != got { + t.Errorf("expected within %t, got %t for %f %f %f %f %f %f %f %f", test.want, got, test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + } + } +} + +func TestBoundingBoxContains(t *testing.T) { + tests := []struct { + lon float64 + lat float64 + minX float64 + minY float64 + maxX float64 + maxY float64 + want bool + }{ + // clearly contains + {1, 1, 0, 0, 2, 2, true}, + // clearly does not contain + {0, 0, 1, 1, 2, 2, false}, + // on corner + {0, 0, 0, 0, 2, 2, true}, + } + for _, test := range tests { + got := BoundingBoxContains(test.lon, test.lat, test.minX, test.minY, test.maxX, test.maxY) + if test.want != got { + t.Errorf("expected box contains %t, got %t for %f,%f in %f %f %f %f ", test.want, got, test.lon, test.lat, test.minX, test.minY, test.maxX, test.maxY) + } + } +} + +func TestNormalizeLon(t *testing.T) { + tests := []struct { + lon float64 + want float64 + }{ + {-180, -180}, + {0, 0}, + {180, 180}, + {181, -179}, + {-181, 179}, + {540, 180}, + } + + for _, test := range tests { + got := normalizeLon(test.lon) + if test.want != got { + t.Errorf("expected normalizedLon %f, got %f for %f", test.want, got, test.lon) + } + } +} + +func TestNormalizeLat(t *testing.T) { + tests := []struct { + lat float64 + want float64 + }{ + {-90, -90}, + {0, 0}, + {90, 90}, + // somewhat unexpected, but double-checked against lucene + {91, 89}, + {-91, -89}, + } + + for _, test := range tests { + got := normalizeLat(test.lat) + if test.want != got { + t.Errorf("expected normalizedLat %f, got %f for %f", test.want, got, test.lat) + } + } +} diff --git a/geo/parse.go b/geo/parse.go index d0585abf..8fdf7230 100644 --- a/geo/parse.go +++ b/geo/parse.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( @@ -6,9 +20,41 @@ import ( ) // ExtractGeoPoint takes an arbitrary interface{} and tries it's best to -// interpret it is as geo point +// interpret it is as geo point. Supportd formats: +// Container: +// slice length 2 (GeoJSON) +// first element lon, second element lat +// map[string]interface{} +// exact keys lat and lon or lng +// struct +// w/exported fields case-insensitive match on lat and lon or lng +// struct +// satisfying Later and Loner or Lnger interfaces +// +// in all cases values must be some sort of numeric-like thing: int/uint/float func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) { var foundLon, foundLat bool + + thingVal := reflect.ValueOf(thing) + thingTyp := thingVal.Type() + + // is it a slice + if thingVal.IsValid() && thingVal.Kind() == reflect.Slice { + // must be length 2 + if thingVal.Len() == 2 { + first := thingVal.Index(0) + if first.CanInterface() { + firstVal := first.Interface() + lon, foundLon = extractNumericVal(firstVal) + } + second := thingVal.Index(1) + if second.CanInterface() { + secondVal := second.Interface() + lat, foundLat = extractNumericVal(secondVal) + } + } + } + // is it a map if l, ok := thing.(map[string]interface{}); ok { if lval, ok := l["lon"]; ok { @@ -23,8 +69,6 @@ func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) { } // now try reflection on struct fields - thingVal := reflect.ValueOf(thing) - thingTyp := thingVal.Type() if thingVal.IsValid() && thingVal.Kind() == reflect.Struct { for i := 0; i < thingVal.NumField(); i++ { field := thingTyp.Field(i) @@ -70,12 +114,17 @@ func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) { // extract numeric value (if possible) and returna s float64 func extractNumericVal(v interface{}) (float64, bool) { - switch v := v.(type) { - case float64: - return v, true - case float32: - return float64(v), true + val := reflect.ValueOf(v) + typ := val.Type() + switch typ.Kind() { + case reflect.Float32, reflect.Float64: + return val.Float(), true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return float64(val.Int()), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return float64(val.Uint()), true } + return 0, false } diff --git a/geo/parse_test.go b/geo/parse_test.go new file mode 100644 index 00000000..4d4a36d5 --- /dev/null +++ b/geo/parse_test.go @@ -0,0 +1,184 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import "testing" + +func TestExtractGeoPoint(t *testing.T) { + + tests := []struct { + in interface{} + lon float64 + lat float64 + success bool + }{ + // values are ints + { + in: map[string]interface{}{ + "lat": 5, + "lon": 5, + }, + lon: 5, + lat: 5, + success: true, + }, + // values are uints + { + in: map[string]interface{}{ + "lat": uint(5), + "lon": uint(5), + }, + lon: 5, + lat: 5, + success: true, + }, + // values float64 as with parsed JSON + { + in: map[string]interface{}{ + "lat": 5.0, + "lon": 5.0, + }, + lon: 5, + lat: 5, + success: true, + }, + // values are bool (not supported) + { + in: map[string]interface{}{ + "lat": true, + "lon": false, + }, + lon: 0, + lat: 0, + success: false, + }, + // using lng variant of lon + { + in: map[string]interface{}{ + "lat": 5.0, + "lng": 5.0, + }, + lon: 5, + lat: 5, + success: true, + }, + // using struct + { + in: struct { + Lon float64 + Lat float64 + }{ + Lon: 3.0, + Lat: 7.5, + }, + lon: 3.0, + lat: 7.5, + success: true, + }, + // struct with lng alterante + { + in: struct { + Lng float64 + Lat float64 + }{ + Lng: 3.0, + Lat: 7.5, + }, + lon: 3.0, + lat: 7.5, + success: true, + }, + // test going throug interface + { + in: &s1{ + lon: 4.0, + lat: 6.9, + }, + lon: 4.0, + lat: 6.9, + success: true, + }, + // test going throug interface with lng variant + { + in: &s2{ + lng: 4.0, + lat: 6.9, + }, + lon: 4.0, + lat: 6.9, + success: true, + }, + // try GeoJSON slice + { + in: []interface{}{3.4, 5.9}, + lon: 3.4, + lat: 5.9, + success: true, + }, + // try GeoJSON slice too long + { + in: []interface{}{3.4, 5.9, 9.4}, + lon: 0, + lat: 0, + success: false, + }, + // slice of floats + { + in: []float64{3.4, 5.9}, + lon: 3.4, + lat: 5.9, + success: true, + }, + } + + for _, test := range tests { + lon, lat, success := ExtractGeoPoint(test.in) + if success != test.success { + t.Errorf("expected extract geo point %t, got %t for %v", test.success, success, test.in) + } + if lon != test.lon { + t.Errorf("expected lon %f, got %f for %v", test.lon, lon, test.in) + } + if lat != test.lat { + t.Errorf("expected lat %f, got %f for %v", test.lat, lat, test.in) + } + } +} + +type s1 struct { + lon float64 + lat float64 +} + +func (s *s1) Lon() float64 { + return s.lon +} + +func (s *s1) Lat() float64 { + return s.lat +} + +type s2 struct { + lng float64 + lat float64 +} + +func (s *s2) Lng() float64 { + return s.lng +} + +func (s *s2) Lat() float64 { + return s.lat +} diff --git a/geo/sloppy.go b/geo/sloppy.go index 5a2ba6bc..ad7306c6 100644 --- a/geo/sloppy.go +++ b/geo/sloppy.go @@ -1,6 +1,22 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo -import "math" +import ( + "math" +) var earthDiameterPerLatitude []float64 var sinTab []float64 @@ -51,6 +67,7 @@ var asinDelta = asinMaxValueForTabs / (asinTabsSize - 1) var asinIndexer = 1 / asinDelta func init() { + // initializes the tables used for the sloppy math functions // sin and cos sinTab = make([]float64, sinCosTabsSize) @@ -119,6 +136,8 @@ func init() { } } +// earthDiameter returns an estimation of the earth's diameter at the specified +// latitude func earthDiameter(lat float64) float64 { index := math.Mod(math.Abs(lat)*radiusIndexer+0.5, float64(len(earthDiameterPerLatitude))) if math.IsNaN(index) { @@ -127,6 +146,7 @@ func earthDiameter(lat float64) float64 { return earthDiameterPerLatitude[int(index)] } +// cos is a sloppy math (faster) implementation of math.Cos func cos(a float64) float64 { if a < 0.0 { a = -a @@ -145,6 +165,7 @@ func cos(a float64) float64 { return indexCos + delta*(-indexSin+delta*(-indexCos*oneDivF2+delta*(indexSin*oneDivF3+delta*indexCos*oneDivF4))) } +// asin is a sloppy math (faster) implementation of math.Asin func asin(a float64) float64 { var negateResult bool if a < 0 { diff --git a/geo/sloppy_test.go b/geo/sloppy_test.go index 4ad4ff3b..4fefdc5b 100644 --- a/geo/sloppy_test.go +++ b/geo/sloppy_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package geo import ( @@ -59,6 +73,9 @@ func TestAsin(t *testing.T) { {0.7071068, math.Pi / 4}, {0.8660254, math.Pi / 3}, {1, math.Pi / 2}, + // these last two cases test the code outside tabular range + {0.999999999999999, math.Pi / 2}, + {-0.999999999999999, -math.Pi / 2}, } for _, test := range tests { @@ -67,7 +84,7 @@ func TestAsin(t *testing.T) { t.Errorf("wanted NaN, got %f for asin(%f)", got, test.in) } if !math.IsNaN(test.want) && math.Abs(got-test.want) > asinDelta { - t.Errorf("wanted: %f, got %f for asin(%f) diff %f", test.want, got, test.in, math.Abs(got-test.want)) + t.Errorf("wanted: %f, got %f for asin(%f) diff %.16f", test.want, got, test.in, math.Abs(got-test.want)) } } } diff --git a/search/searcher/search_geoboundingbox.go b/search/searcher/search_geoboundingbox.go index 8d3d6249..bde68c7c 100644 --- a/search/searcher/search_geoboundingbox.go +++ b/search/searcher/search_geoboundingbox.go @@ -39,6 +39,12 @@ type GeoBoundingBoxSearcher struct { } func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat, maxLon, maxLat float64, field string, boost float64, options search.SearcherOptions) (*GeoBoundingBoxSearcher, error) { + var openedSearchers []search.Searcher + cleanupOpenedSearchers := func() { + for _, s := range openedSearchers { + _ = s.Close() + } + } rv := &GeoBoundingBoxSearcher{ indexReader: indexReader, minLon: minLon, @@ -55,12 +61,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat, ma for _, r := range rv.rangeBounds { ts, err := NewTermSearcher(indexReader, string(r.cell), field, 1.0, options) if err != nil { - for _, s := range termsOnBoundary { - _ = s.Close() - } - for _, s := range termsNotOnBoundary { - _ = s.Close() - } + cleanupOpenedSearchers() return nil, err } if r.boundary { @@ -68,15 +69,11 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat, ma } else { termsNotOnBoundary = append(termsNotOnBoundary, ts) } + openedSearchers = append(openedSearchers) } onBoundarySearcher, err := NewDisjunctionSearcher(indexReader, termsOnBoundary, 0, options) if err != nil { - for _, s := range termsOnBoundary { - _ = s.Close() - } - for _, s := range termsNotOnBoundary { - _ = s.Close() - } + cleanupOpenedSearchers() return nil, err } filterOnBoundarySearcher := NewFilteringSearcher(onBoundarySearcher, func(d *search.DocumentMatch) bool { @@ -102,28 +99,17 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat, ma } return false }) + openedSearchers = append(openedSearchers, filterOnBoundarySearcher) notOnBoundarySearcher, err := NewDisjunctionSearcher(indexReader, termsNotOnBoundary, 0, options) if err != nil { - for _, s := range termsOnBoundary { - _ = s.Close() - } - for _, s := range termsNotOnBoundary { - _ = s.Close() - } - _ = filterOnBoundarySearcher.Close() + cleanupOpenedSearchers() return nil, err } + openedSearchers = append(openedSearchers, notOnBoundarySearcher) rv.searcher, err = NewDisjunctionSearcher(indexReader, []search.Searcher{filterOnBoundarySearcher, notOnBoundarySearcher}, 0, options) if err != nil { - for _, s := range termsOnBoundary { - _ = s.Close() - } - for _, s := range termsNotOnBoundary { - _ = s.Close() - } - _ = filterOnBoundarySearcher.Close() - _ = notOnBoundarySearcher.Close() + cleanupOpenedSearchers() return nil, err } return rv, nil @@ -185,26 +171,14 @@ func (s *GeoBoundingBoxSearcher) relateAndRecurse(start, end uint64, res uint) { level := ((geo.GeoBits << 1) - res) >> 1 - within := res%document.GeoPrecisionStep == 0 && s.cellWithin(minLon, minLat, maxLon, maxLat) - if within || (level == geoDetailLevel && s.cellIntersectShape(minLon, minLat, maxLon, maxLat)) { + within := res%document.GeoPrecisionStep == 0 && geo.RectWithin(minLon, minLat, maxLon, maxLat, s.minLon, s.minLat, s.maxLon, s.maxLat) + if within || (level == geoDetailLevel && geo.RectIntersects(minLon, minLat, maxLon, maxLat, s.minLon, s.minLat, s.maxLon, s.maxLat)) { s.rangeBounds = append(s.rangeBounds, newGeoRange(start, res, level, !within)) - } else if level < geoDetailLevel && s.cellIntersectsMBR(minLon, minLat, maxLon, maxLat) { + } else if level < geoDetailLevel && geo.RectIntersects(minLon, minLat, maxLon, maxLat, s.minLon, s.minLat, s.maxLon, s.maxLat) { s.computeRange(start, res-1) } } -func (s *GeoBoundingBoxSearcher) cellWithin(minLon, minLat, maxLon, maxLat float64) bool { - return geo.RectWithin(minLon, minLat, maxLon, maxLat, s.minLon, s.minLat, s.maxLon, s.maxLat) -} - -func (s *GeoBoundingBoxSearcher) cellIntersectShape(minLon, minLat, maxLon, maxLat float64) bool { - return s.cellIntersectsMBR(minLon, minLat, maxLon, maxLat) -} - -func (s *GeoBoundingBoxSearcher) cellIntersectsMBR(minLon, minLat, maxLon, maxLat float64) bool { - return geo.RectIntersects(minLon, minLat, maxLon, maxLat, s.minLon, s.minLat, s.maxLon, s.maxLat) -} - type geoRange struct { cell []byte level uint