snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 22:27:16 +05:00
commit f8d3d9f0cf
797 changed files with 159570 additions and 0 deletions

312
geo/README.md Normal file
View File

@@ -0,0 +1,312 @@
# Geo spatial search support in bleve
Latest bleve spatial capabilities are powered by spatial hierarchical tokens generated from s2geometry.
You can find more details about the [s2geometry basics here](http://s2geometry.io/), and explore the extended functionality of our forked golang port of [s2geometry lib here](https://github.com/blevesearch/geo).
Users can continue to index and query `geopoint` field type and the existing queries like,
- Point Distance
- Bounded Rectangle
- Bounded Polygon
as before.
## New Spatial Field Type - geoshape
We have introduced a field type (`geoshape`) for representing the new spatial types.
Using the new `geoshape` field type, users can unblock the spatial capabilities
for the [geojson](https://datatracker.ietf.org/doc/html/rfc7946) shapes like,
- Point
- LineString
- Polygon
- MultiPoint
- MultiLineString
- MultiPolygon
- GeometryCollection
In addition to these shapes, bleve will also support additional shapes like,
- Circle
- Envelope (Bounded box)
To specify GeoJSON data, use a nested field with:
- a field named type that specifies the GeoJSON object type and the type value will be case-insensitive.
- a field named coordinates that specifies the object's coordinates.
```text
"fieldName": {
"type": "GeoJSON Type",
"coordinates": <coordinates>
}
```
- If specifying latitude and longitude coordinates, list the longitude first and then latitude.
- Valid longitude values are between -180 and 180, both inclusive.
- Valid latitude values are between -90 and 90, both inclusive.
- Shapes would be internally represented as geodesics.
- The GeoJSON specification strongly suggests splitting geometries so that neither of their parts crosses the antimeridian.
Examples for the various geojson shapes representations are as below.
## Point
The following specifies a [Point](https://tools.ietf.org/html/rfc7946#section-3.1.2) field in a document:
```json
{
"type": "point",
"coordinates": [75.05687713623047, 22.53539059204079]
}
```
## Linestring
The following specifies a [Linestring](https://tools.ietf.org/html/rfc7946#section-3.1.4) field in a document:
```json
{
"type": "linestring",
"coordinates": [
[77.01416015625, 23.0797317624497],
[78.134765625, 20.385825381874263]
]
}
```
## Polygon
The following specifies a [Polygon](https://tools.ietf.org/html/rfc7946#section-3.1.6) field in a document:
```json
{
"type": "polygon",
"coordinates": [
[
[85.605, 57.207],
[86.396, 55.998],
[87.033, 56.716],
[85.605, 57.207]
]
]
}
```
The first and last coordinates must match in order to close the polygon.
And the exterior coordinates have to be in Counter Clockwise Order in a polygon. (CCW)
## MultiPoint
The following specifies a [Multipoint](https://tools.ietf.org/html/rfc7946#section-3.1.3) field in a document:
```json
{
"type": "multipoint",
"coordinates": [
[-115.8343505859375, 38.45789034424927],
[-115.81237792968749, 38.19502155795575],
[-120.80017089843749, 36.54053616262899],
[-120.67932128906249, 36.33725319397006]
]
}
```
## MultiLineString
The following specifies a [MultiLineString](https://tools.ietf.org/html/rfc7946#section-3.1.5) field in a document:
```json
{
"type": "multilinestring",
"coordinates": [
[
[-118.31726074, 35.250105158],
[-117.509765624, 35.3756141]
],
[
[-118.696289, 34.624167789],
[-118.317260742, 35.03899204]
],
[
[-117.9492187, 35.146862906],
[-117.6745605, 34.41144164]
]
]
}
```
## MultiPolygon
The following specifies a [MultiPolygon](https://tools.ietf.org/html/rfc7946#section-3.1.7) field in a document:
```json
{
"type": "multipolygon",
"coordinates": [
[
[
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.9814, 40.7681],
[-73.958, 40.8003]
]
],
[
[
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.958, 40.8003]
]
]
]
}
```
## GeometryCollection
The following specifies a [GeometryCollection](https://tools.ietf.org/html/rfc7946#section-3.1.8) field in a document:
```json
{
"type": "geometrycollection",
"geometries": [
{
"type": "multipoint",
"coordinates": [
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.9814, 40.7681]
]
},
{
"type": "multilinestring",
"coordinates": [
[
[-73.96943, 40.78519],
[-73.96082, 40.78095]
],
[
[-73.96415, 40.79229],
[-73.95544, 40.78854]
],
[
[-73.97162, 40.78205],
[-73.96374, 40.77715]
],
[
[-73.9788, 40.77247],
[-73.97036, 40.76811]
]
]
},
{
"type": "polygon",
"coordinates": [
[
[0, 0],
[3, 6],
[6, 1],
[0, 0]
],
[
[2, 2],
[3, 3],
[4, 2],
[2, 2]
]
]
}
]
}
```
## Circle
If the user wishes to cover a circular region over the earth's surface, then they could use this shape.
A sample circular shape is as below.
```json
{
"type": "circle",
"coordinates": [75.05687713623047, 22.53539059204079],
"radius": "1000m"
}
```
Circle is specified over the center point coordinates along with the radius.
Example formats supported for radius are:
"5in" , "5inch" , "7yd" , "7yards", "9ft" , "9feet", "11km", "11kilometers", "3nm", "3nauticalmiles", "13mm" , "13millimeters", "15cm", "15centimeters", "17mi", "17miles", "19m" or "19meters".
If the unit cannot be determined, the entire string is parsed and the unit of meters is assumed.
## Envelope
Envelope type, which consists of coordinates for upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]].
```json
{
"type": "envelope",
"coordinates": [
[72.83, 18.979],
[78.508, 17.4555]
]
}
```
## GeoShape Query
Geoshape query support three types/filters of spatial querying capability across those heterogeneous types of documents indexed.
### Query Structure
```json
{
"query": {
"geometry": {
"shape": {
"type": "<shapeType>",
"coordinates": [
[[]]
]
},
"relation": "<<filterName>>"
}
}
}
```
*shapeType* => can be any of the aforementioned types like Point, LineString, Polygon, MultiPoint,
Geometrycollection, MultiLineString, MultiPolygon, Circle and Envelope.
*filterName* => can be any of the 3 types like *intersects*, *contains* and *within*.
### Relation
| FilterName | Description |
| :-----------:| :-----------------------------------------------------------------: |
| `intersects` | Return all documents whose shape field intersects the query geometry. |
| `contains` | Return all documents whose shape field contains the query geometry |
| `within` | Return all documents whose shape field is within the query geometry. |
------------------------------------------------------------------------------------------------------------------------
### Older Implementation
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.
- Points and MultiPoints may only contain Points and MultiPoints.
- LineStrings and MultiLineStrings may only contain Points and MultiPoints.
- Polygons or MultiPolygons intersecting Polygons and MultiPolygons may return arbitrary results when the overlap is only an edge or a vertex.
- Circles containing polygon will return a false positive result if all of the vertices of the polygon are within the circle, but the orientation of those points are clock-wise.
- The edges of an Envelope follows the latitude and longitude lines instead of the shortest path on a globe.
- Envelope intersecting queries with LineStrings, MultiLineStrings, Polygons and MultiPolygons implicitly converts the Envelope into a Polygon which changes the curvature of the edges causing inaccurate results for few edge cases.

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2019 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 BenchmarkGeoHashLen5NewDecode(b *testing.B) {
b.ResetTimer()
hash := "d3hn3"
for i := 0; i < b.N; i++ {
_, _ = DecodeGeoHash(hash)
}
}
func BenchmarkGeoHashLen6NewDecode(b *testing.B) {
b.ResetTimer()
hash := "u4pruy"
for i := 0; i < b.N; i++ {
_, _ = DecodeGeoHash(hash)
}
}
func BenchmarkGeoHashLen7NewDecode(b *testing.B) {
b.ResetTimer()
hash := "u4pruyd"
for i := 0; i < b.N; i++ {
_, _ = DecodeGeoHash(hash)
}
}

210
geo/geo.go Normal file
View File

@@ -0,0 +1,210 @@
// 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 (
"fmt"
"math"
"github.com/blevesearch/bleve/v2/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 maxLon = 180.0
var maxLat = 90.0
var minLonRad = minLon * degreesToRadian
var minLatRad = minLat * degreesToRadian
var maxLonRad = maxLon * degreesToRadian
var maxLatRad = maxLat * degreesToRadian
var geoTolerance = 1e-6
var lonScale = float64((uint64(0x1)<<GeoBits)-1) / 360.0
var latScale = float64((uint64(0x1)<<GeoBits)-1) / 180.0
var geoHashMaxLength = 12
// Point represents a geo point.
type Point struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
}
// MortonHash computes the morton hash value for the provided geo point
// This point is ordered as lon, lat.
func MortonHash(lon, lat float64) uint64 {
return numeric.Interleave(scaleLon(lon), scaleLat(lat))
}
func scaleLon(lon float64) uint64 {
rv := uint64((lon - minLon) * lonScale)
return rv
}
func scaleLat(lat float64) uint64 {
rv := uint64((lat - minLat) * latScale)
return rv
}
// MortonUnhashLon extracts the longitude value from the provided morton hash.
func MortonUnhashLon(hash uint64) float64 {
return unscaleLon(numeric.Deinterleave(hash))
}
// MortonUnhashLat extracts the latitude value from the provided morton hash.
func MortonUnhashLat(hash uint64) float64 {
return unscaleLat(numeric.Deinterleave(hash >> 1))
}
func unscaleLon(lon uint64) float64 {
return (float64(lon) / lonScale) + minLon
}
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 {
return 0
}
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
}
const degreesToRadian = math.Pi / 180
const radiansToDegrees = 180 / math.Pi
// DegreesToRadians converts an angle in degrees to radians
func DegreesToRadians(d float64) float64 {
return d * degreesToRadian
}
// RadiansToDegrees converts an angle in radians to degrees
func RadiansToDegrees(r float64) float64 {
return r * radiansToDegrees
}
var earthMeanRadiusMeters = 6371008.7714
func RectFromPointDistance(lon, lat, dist float64) (float64, float64, float64, float64, error) {
err := checkLongitude(lon)
if err != nil {
return 0, 0, 0, 0, err
}
err = checkLatitude(lat)
if err != nil {
return 0, 0, 0, 0, err
}
radLon := DegreesToRadians(lon)
radLat := DegreesToRadians(lat)
radDistance := (dist + 7e-2) / earthMeanRadiusMeters
minLatL := radLat - radDistance
maxLatL := radLat + radDistance
var minLonL, maxLonL float64
if minLatL > minLatRad && maxLatL < maxLatRad {
deltaLon := math.Asin(math.Sin(radDistance) / math.Cos(radLat))
minLonL = radLon - deltaLon
if minLonL < minLonRad {
minLonL += 2 * math.Pi
}
maxLonL = radLon + deltaLon
if maxLonL > maxLonRad {
maxLonL -= 2 * math.Pi
}
} else {
// pole is inside distance
minLatL = math.Max(minLatL, minLatRad)
maxLatL = math.Min(maxLatL, maxLatRad)
minLonL = minLonRad
maxLonL = maxLonRad
}
return RadiansToDegrees(minLonL),
RadiansToDegrees(maxLatL),
RadiansToDegrees(maxLonL),
RadiansToDegrees(minLatL),
nil
}
func checkLatitude(latitude float64) error {
if math.IsNaN(latitude) || latitude < minLat || latitude > maxLat {
return fmt.Errorf("invalid latitude %f; must be between %f and %f", latitude, minLat, maxLat)
}
return nil
}
func checkLongitude(longitude float64) error {
if math.IsNaN(longitude) || longitude < minLon || longitude > maxLon {
return fmt.Errorf("invalid longitude %f; must be between %f and %f", longitude, minLon, maxLon)
}
return nil
}
func BoundingRectangleForPolygon(polygon []Point) (
float64, float64, float64, float64, error) {
err := checkLongitude(polygon[0].Lon)
if err != nil {
return 0, 0, 0, 0, err
}
err = checkLatitude(polygon[0].Lat)
if err != nil {
return 0, 0, 0, 0, err
}
maxY, minY := polygon[0].Lat, polygon[0].Lat
maxX, minX := polygon[0].Lon, polygon[0].Lon
for i := 1; i < len(polygon); i++ {
err := checkLongitude(polygon[i].Lon)
if err != nil {
return 0, 0, 0, 0, err
}
err = checkLatitude(polygon[i].Lat)
if err != nil {
return 0, 0, 0, 0, err
}
maxY = math.Max(maxY, polygon[i].Lat)
minY = math.Min(minY, polygon[i].Lat)
maxX = math.Max(maxX, polygon[i].Lon)
minX = math.Min(minX, polygon[i].Lon)
}
return minX, maxY, maxX, minY, nil
}

98
geo/geo_dist.go Normal file
View File

@@ -0,0 +1,98 @@
// 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 (
"fmt"
"math"
"strconv"
"strings"
)
type distanceUnit struct {
conv float64
suffixes []string
}
var inch = distanceUnit{0.0254, []string{"in", "inch"}}
var yard = distanceUnit{0.9144, []string{"yd", "yards"}}
var feet = distanceUnit{0.3048, []string{"ft", "feet"}}
var kilom = distanceUnit{1000, []string{"km", "kilometers"}}
var nauticalm = distanceUnit{1852.0, []string{"nm", "nauticalmiles"}}
var millim = distanceUnit{0.001, []string{"mm", "millimeters"}}
var centim = distanceUnit{0.01, []string{"cm", "centimeters"}}
var miles = distanceUnit{1609.344, []string{"mi", "miles"}}
var meters = distanceUnit{1, []string{"m", "meters"}}
var distanceUnits = []*distanceUnit{
&inch, &yard, &feet, &kilom, &nauticalm, &millim, &centim, &miles, &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 {
if strings.HasSuffix(d, unitSuffix) {
parsedNum, err := strconv.ParseFloat(d[0:len(d)-len(unitSuffix)], 64)
if err != nil {
return 0, err
}
return parsedNum * unit.conv, nil
}
}
}
// no unit matched, try assuming meters?
parsedNum, err := strconv.ParseFloat(d, 64)
if err != nil {
return 0, err
}
return parsedNum, nil
}
// ParseDistanceUnit attempts to parse a distance unit and return the
// multiplier for converting this to meters. If the unit cannot be parsed
// then 0 and the error message is returned.
func ParseDistanceUnit(u string) (float64, error) {
for _, unit := range distanceUnits {
for _, unitSuffix := range unit.suffixes {
if u == unitSuffix {
return unit.conv, nil
}
}
}
return 0, fmt.Errorf("unknown distance unit: %s", u)
}
// Haversin computes the distance between two points.
// This implementation uses the sloppy math implementations which trade off
// accuracy for performance. The distance returned is in kilometers.
func Haversin(lon1, lat1, lon2, lat2 float64) float64 {
x1 := lat1 * degreesToRadian
x2 := lat2 * degreesToRadian
h1 := 1 - math.Cos(x1-x2)
h2 := 1 - math.Cos((lon1-lon2)*degreesToRadian)
h := (h1 + math.Cos(x1)*math.Cos(x2)*h2) / 2
avgLat := (x1 + x2) / 2
diameter := earthDiameter(avgLat)
return diameter * math.Asin(math.Min(1, math.Sqrt(h)))
}

120
geo/geo_dist_test.go Normal file
View File

@@ -0,0 +1,120 @@
// 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 (
"fmt"
"math"
"reflect"
"strconv"
"testing"
)
func TestParseDistance(t *testing.T) {
tests := []struct {
dist string
want float64
wantErr error
}{
{"5mi", 5 * 1609.344, nil},
{"3", 3, nil},
{"3m", 3, nil},
{"5km", 5000, nil},
{"km", 0, &strconv.NumError{Func: "ParseFloat", Num: "", Err: strconv.ErrSyntax}},
{"", 0, &strconv.NumError{Func: "ParseFloat", Num: "", Err: strconv.ErrSyntax}},
}
for _, test := range tests {
got, err := ParseDistance(test.dist)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("expected err: %v, got %v for %s", test.wantErr, err, test.dist)
}
if got != test.want {
t.Errorf("expected distance %f got %f for %s", test.want, got, test.dist)
}
}
}
func TestParseDistanceUnit(t *testing.T) {
tests := []struct {
dist string
want float64
wantErr error
}{
{"mi", 1609.344, nil},
{"m", 1, nil},
{"km", 1000, nil},
{"", 0, fmt.Errorf("unknown distance unit: ")},
{"kam", 0, fmt.Errorf("unknown distance unit: kam")},
}
for _, test := range tests {
got, err := ParseDistanceUnit(test.dist)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("expected err: %v, got %v for %s", test.wantErr, err, test.dist)
}
if got != test.want {
t.Errorf("expected distance %f got %f for %s", test.want, got, test.dist)
}
}
}
func TestHaversinDistance(t *testing.T) {
earthRadiusKMs := 6378.137
halfCircle := earthRadiusKMs * math.Pi
tests := []struct {
lon1 float64
lat1 float64
lon2 float64
lat2 float64
want float64
}{
{1, 1, math.NaN(), 1, math.NaN()},
{1, 1, 1, math.NaN(), math.NaN()},
{1, math.NaN(), 1, 1, math.NaN()},
{math.NaN(), 1, 1, 1, math.NaN()},
{0, 0, 0, 0, 0},
{-180, 0, -180, 0, 0},
{-180, 0, 180, 0, 0},
{180, 0, 180, 0, 0},
{0, 90, 0, 90, 0},
{-180, 90, -180, 90, 0},
{-180, 90, 180, 90, 0},
{180, 90, 180, 90, 0},
{0, 0, 180, 0, halfCircle},
{-74.0059731, 40.7143528, -74.0059731, 40.7143528, 0},
{-74.0059731, 40.7143528, -73.9844722, 40.759011, 5.286},
{-74.0059731, 40.7143528, -74.007819, 40.718266, 0.4621},
{-74.0059731, 40.7143528, -74.0088305, 40.7051157, 1.055},
{-74.0059731, 40.7143528, -74, 40.7247222, 1.258},
{-74.0059731, 40.7143528, -73.9962255, 40.731033, 2.029},
{-74.0059731, 40.7143528, -73.95, 40.65, 8.572},
}
for _, test := range tests {
got := Haversin(test.lon1, test.lat1, test.lon2, test.lat2)
if math.IsNaN(test.want) && !math.IsNaN(got) {
t.Errorf("expected NaN, got %f", got)
}
if !math.IsNaN(test.want) && math.Abs(got-test.want) > 1e-2 {
t.Errorf("expected %f got %f", test.want, got)
}
}
}

523
geo/geo_s2plugin_impl.go Normal file
View File

@@ -0,0 +1,523 @@
// Copyright (c) 2022 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 (
"encoding/json"
"sync"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
"github.com/blevesearch/geo/geojson"
"github.com/blevesearch/geo/s2"
)
const (
PointType = "point"
MultiPointType = "multipoint"
LineStringType = "linestring"
MultiLineStringType = "multilinestring"
PolygonType = "polygon"
MultiPolygonType = "multipolygon"
GeometryCollectionType = "geometrycollection"
CircleType = "circle"
EnvelopeType = "envelope"
)
// spatialPluginsMap is spatial plugin cache.
var (
spatialPluginsMap = make(map[string]index.SpatialAnalyzerPlugin)
pluginsMapLock = sync.RWMutex{}
)
func init() {
registerS2RegionTermIndexer()
}
func registerS2RegionTermIndexer() {
spatialPlugin := S2SpatialAnalyzerPlugin{
s2Indexer: s2.NewRegionTermIndexerWithOptions(initS2IndexerOptions()),
s2Searcher: s2.NewRegionTermIndexerWithOptions(initS2SearcherOptions()),
s2GeoPointsRegionTermIndexer: s2.NewRegionTermIndexerWithOptions(initS2OptionsForGeoPoints()),
}
RegisterSpatialAnalyzerPlugin(&spatialPlugin)
}
// RegisterSpatialAnalyzerPlugin registers the given plugin implementation.
func RegisterSpatialAnalyzerPlugin(plugin index.SpatialAnalyzerPlugin) {
pluginsMapLock.Lock()
spatialPluginsMap[plugin.Type()] = plugin
pluginsMapLock.Unlock()
}
// GetSpatialAnalyzerPlugin retrieves the given implementation type.
func GetSpatialAnalyzerPlugin(typ string) index.SpatialAnalyzerPlugin {
pluginsMapLock.RLock()
rv := spatialPluginsMap[typ]
pluginsMapLock.RUnlock()
return rv
}
// initS2IndexerOptions returns the options for s2's region
// term indexer for the index time tokens of geojson shapes.
func initS2IndexerOptions() s2.Options {
options := s2.Options{}
// maxLevel control the maximum size of the
// S2Cells used to approximate regions.
options.SetMaxLevel(16)
// minLevel control the minimum size of the
// S2Cells used to approximate regions.
options.SetMinLevel(2)
// levelMod value greater than 1 increases the effective branching
// factor of the S2Cell hierarchy by skipping some levels.
options.SetLevelMod(1)
// maxCells controls the maximum number of cells
// when approximating each s2 region.
options.SetMaxCells(20)
return options
}
// initS2SearcherOptions returns the options for s2's region
// term indexer for the query time tokens of geojson shapes.
func initS2SearcherOptions() s2.Options {
options := s2.Options{}
// maxLevel control the maximum size of the
// S2Cells used to approximate regions.
options.SetMaxLevel(16)
// minLevel control the minimum size of the
// S2Cells used to approximate regions.
options.SetMinLevel(2)
// levelMod value greater than 1 increases the effective branching
// factor of the S2Cell hierarchy by skipping some levels.
options.SetLevelMod(1)
// maxCells controls the maximum number of cells
// when approximating each s2 region.
options.SetMaxCells(8)
return options
}
// initS2OptionsForGeoPoints returns the options for
// s2's region term indexer for the original geopoints.
func initS2OptionsForGeoPoints() s2.Options {
options := s2.Options{}
// maxLevel control the maximum size of the
// S2Cells used to approximate regions.
options.SetMaxLevel(16)
// minLevel control the minimum size of the
// S2Cells used to approximate regions.
options.SetMinLevel(4)
// levelMod value greater than 1 increases the effective branching
// factor of the S2Cell hierarchy by skipping some levels.
options.SetLevelMod(2)
// maxCells controls the maximum number of cells
// when approximating each s2 region.
options.SetMaxCells(8)
// explicit for geo points.
options.SetPointsOnly(true)
return options
}
// S2SpatialAnalyzerPlugin is an implementation of
// the index.SpatialAnalyzerPlugin interface.
type S2SpatialAnalyzerPlugin struct {
s2Indexer *s2.RegionTermIndexer
s2Searcher *s2.RegionTermIndexer
s2GeoPointsRegionTermIndexer *s2.RegionTermIndexer
}
func (s *S2SpatialAnalyzerPlugin) Type() string {
return "s2"
}
func (s *S2SpatialAnalyzerPlugin) GetIndexTokens(queryShape index.GeoJSON) []string {
var rv []string
shapes := []index.GeoJSON{queryShape}
if gc, ok := queryShape.(*geojson.GeometryCollection); ok {
shapes = gc.Shapes
}
for _, shape := range shapes {
if s2t, ok := shape.(s2Tokenizable); ok {
rv = append(rv, s2t.IndexTokens(s.s2Indexer)...)
} else if s2t, ok := shape.(s2TokenizableEx); ok {
rv = append(rv, s2t.IndexTokens(s)...)
}
}
return geojson.DeduplicateTerms(rv)
}
func (s *S2SpatialAnalyzerPlugin) GetQueryTokens(queryShape index.GeoJSON) []string {
var rv []string
shapes := []index.GeoJSON{queryShape}
if gc, ok := queryShape.(*geojson.GeometryCollection); ok {
shapes = gc.Shapes
}
for _, shape := range shapes {
if s2t, ok := shape.(s2Tokenizable); ok {
rv = append(rv, s2t.QueryTokens(s.s2Searcher)...)
} else if s2t, ok := shape.(s2TokenizableEx); ok {
rv = append(rv, s2t.QueryTokens(s)...)
}
}
return geojson.DeduplicateTerms(rv)
}
// ------------------------------------------------------------------------
// s2Tokenizable is an optional interface for shapes that support
// the generation of s2 based tokens that can be used for both
// indexing and querying.
type s2Tokenizable interface {
// IndexTokens returns the tokens for indexing.
IndexTokens(*s2.RegionTermIndexer) []string
// QueryTokens returns the tokens for searching.
QueryTokens(*s2.RegionTermIndexer) []string
}
// ------------------------------------------------------------------------
// s2TokenizableEx is an optional interface for shapes that support
// the generation of s2 based tokens that can be used for both
// indexing and querying. This is intended for the older geopoint
// indexing and querying.
type s2TokenizableEx interface {
// IndexTokens returns the tokens for indexing.
IndexTokens(*S2SpatialAnalyzerPlugin) []string
// QueryTokens returns the tokens for searching.
QueryTokens(*S2SpatialAnalyzerPlugin) []string
}
//----------------------------------------------------------------------------------
func (p *Point) Type() string {
return PointType
}
func (p *Point) Value() ([]byte, error) {
return util.MarshalJSON(p)
}
func (p *Point) Intersects(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (p *Point) Contains(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (p *Point) IndexTokens(s *S2SpatialAnalyzerPlugin) []string {
return s.s2GeoPointsRegionTermIndexer.GetIndexTermsForPoint(s2.PointFromLatLng(
s2.LatLngFromDegrees(p.Lat, p.Lon)), "")
}
func (p *Point) QueryTokens(s *S2SpatialAnalyzerPlugin) []string {
return nil
}
func (p *Point) IndexCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (p *Point) QueryCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (p *Point) BoundingBox() index.GeoJSON {
// placeholder implementation
return nil
}
//----------------------------------------------------------------------------------
type boundedRectangle struct {
minLat float64
maxLat float64
minLon float64
maxLon float64
}
func NewBoundedRectangle(minLat, minLon, maxLat,
maxLon float64) *boundedRectangle {
return &boundedRectangle{minLat: minLat,
maxLat: maxLat, minLon: minLon, maxLon: maxLon}
}
func (br *boundedRectangle) Type() string {
// placeholder implementation
return "boundedRectangle"
}
func (br *boundedRectangle) Value() ([]byte, error) {
return util.MarshalJSON(br)
}
func (p *boundedRectangle) Intersects(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (p *boundedRectangle) Contains(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (br *boundedRectangle) IndexTokens(s *S2SpatialAnalyzerPlugin) []string {
return nil
}
func (br *boundedRectangle) QueryTokens(s *S2SpatialAnalyzerPlugin) []string {
rect := s2.RectFromDegrees(br.minLat, br.minLon, br.maxLat, br.maxLon)
// obtain the terms to be searched for the given bounding box.
terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion(rect, "")
return geojson.StripCoveringTerms(terms)
}
func (br *boundedRectangle) IndexCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (br *boundedRectangle) QueryCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (br *boundedRectangle) BoundingBox() index.GeoJSON {
// placeholder implementation
return nil
}
//----------------------------------------------------------------------------------
type boundedPolygon struct {
coordinates []Point
}
func NewBoundedPolygon(coordinates []Point) *boundedPolygon {
return &boundedPolygon{coordinates: coordinates}
}
func (bp *boundedPolygon) Type() string {
// placeholder implementation
return "boundedPolygon"
}
func (bp *boundedPolygon) Value() ([]byte, error) {
return util.MarshalJSON(bp)
}
func (p *boundedPolygon) Intersects(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (p *boundedPolygon) Contains(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (bp *boundedPolygon) IndexTokens(s *S2SpatialAnalyzerPlugin) []string {
return nil
}
func (bp *boundedPolygon) QueryTokens(s *S2SpatialAnalyzerPlugin) []string {
vertices := make([]s2.Point, len(bp.coordinates))
for i, point := range bp.coordinates {
vertices[i] = s2.PointFromLatLng(
s2.LatLngFromDegrees(point.Lat, point.Lon))
}
s2polygon := s2.PolygonFromOrientedLoops([]*s2.Loop{s2.LoopFromPoints(vertices)})
// obtain the terms to be searched for the given polygon.
terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion(
s2polygon.CapBound(), "")
return geojson.StripCoveringTerms(terms)
}
func (bp *boundedPolygon) IndexCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (bp *boundedPolygon) QueryCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (bp *boundedPolygon) BoundingBox() index.GeoJSON {
// placeholder implementation
return nil
}
//----------------------------------------------------------------------------------
type pointDistance struct {
dist float64
centerLat float64
centerLon float64
}
func (p *pointDistance) Type() string {
// placeholder implementation
return "pointDistance"
}
func (p *pointDistance) Value() ([]byte, error) {
return util.MarshalJSON(p)
}
func NewPointDistance(centerLat, centerLon,
dist float64) *pointDistance {
return &pointDistance{centerLat: centerLat,
centerLon: centerLon, dist: dist}
}
func (p *pointDistance) Intersects(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (p *pointDistance) Contains(s index.GeoJSON) (bool, error) {
// placeholder implementation
return false, nil
}
func (pd *pointDistance) IndexTokens(s *S2SpatialAnalyzerPlugin) []string {
return nil
}
func (pd *pointDistance) QueryTokens(s *S2SpatialAnalyzerPlugin) []string {
// obtain the covering query region from the given points.
queryRegion := s2.CapFromCenterAndRadius(pd.centerLat,
pd.centerLon, pd.dist)
// obtain the query terms for the query region.
terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion(queryRegion, "")
return geojson.StripCoveringTerms(terms)
}
func (pd *pointDistance) IndexCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (pd *pointDistance) QueryCells() ([]uint64, []uint64) {
// placeholder implementation
return nil, nil
}
func (pd *pointDistance) BoundingBox() index.GeoJSON {
// placeholder implementation
return nil
}
// ------------------------------------------------------------------------
// NewGeometryCollection instantiate a geometrycollection
// and prefix the byte contents with certain glue bytes that
// can be used later while filering the doc values.
func NewGeometryCollection(coordinates [][][][][]float64,
typs []string) (index.GeoJSON, []byte, error) {
shapes := make([]*geojson.GeoShape, len(coordinates))
for i := range coordinates {
shapes[i] = &geojson.GeoShape{
Coordinates: coordinates[i],
Type: typs[i],
}
}
return geojson.NewGeometryCollection(shapes)
}
func NewGeometryCollectionFromShapes(shapes []*geojson.GeoShape) (
index.GeoJSON, []byte, error) {
return geojson.NewGeometryCollection(shapes)
}
// NewGeoCircleShape instantiate a circle shape and
// prefix the byte contents with certain glue bytes that
// can be used later while filering the doc values.
func NewGeoCircleShape(cp []float64,
radius string) (index.GeoJSON, []byte, error) {
return geojson.NewGeoCircleShape(cp, radius)
}
func NewGeoJsonShape(coordinates [][][][]float64, typ string) (
index.GeoJSON, []byte, error) {
return geojson.NewGeoJsonShape(coordinates, typ)
}
func NewGeoJsonPoint(points []float64) index.GeoJSON {
return geojson.NewGeoJsonPoint(points)
}
func NewGeoJsonMultiPoint(points [][]float64) index.GeoJSON {
return geojson.NewGeoJsonMultiPoint(points)
}
func NewGeoJsonLinestring(points [][]float64) index.GeoJSON {
return geojson.NewGeoJsonLinestring(points)
}
func NewGeoJsonMultilinestring(points [][][]float64) index.GeoJSON {
return geojson.NewGeoJsonMultilinestring(points)
}
func NewGeoJsonPolygon(points [][][]float64) index.GeoJSON {
return geojson.NewGeoJsonPolygon(points)
}
func NewGeoJsonMultiPolygon(points [][][][]float64) index.GeoJSON {
return geojson.NewGeoJsonMultiPolygon(points)
}
func NewGeoCircle(points []float64, radius string) index.GeoJSON {
return geojson.NewGeoCircle(points, radius)
}
func NewGeoEnvelope(points [][]float64) index.GeoJSON {
return geojson.NewGeoEnvelope(points)
}
func ParseGeoJSONShape(input json.RawMessage) (index.GeoJSON, error) {
return geojson.ParseGeoJSONShape(input)
}

183
geo/geo_test.go Normal file
View File

@@ -0,0 +1,183 @@
// 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"
"testing"
)
func TestMortonHashMortonUnhash(t *testing.T) {
tests := []struct {
lon float64
lat float64
}{
{-180.0, -90.0},
{-5, 27.3},
{0, 0},
{1.0, 1.0},
{24.7, -80.4},
{180.0, 90.0},
}
for _, test := range tests {
hash := MortonHash(test.lon, test.lat)
lon := MortonUnhashLon(hash)
lat := MortonUnhashLat(hash)
if compareGeo(test.lon, lon) != 0 {
t.Errorf("expected lon %f, got %f, hash %x", test.lon, lon, hash)
}
if compareGeo(test.lat, lat) != 0 {
t.Errorf("expected lat %f, got %f, hash %x", test.lat, lat, hash)
}
}
}
func TestScaleLonUnscaleLon(t *testing.T) {
tests := []struct {
lon float64
}{
{-180.0},
{0.0},
{1.0},
{180.0},
}
for _, test := range tests {
s := scaleLon(test.lon)
lon := unscaleLon(s)
if compareGeo(test.lon, lon) != 0 {
t.Errorf("expected %f, got %f, scaled was %d", test.lon, lon, s)
}
}
}
func TestScaleLatUnscaleLat(t *testing.T) {
tests := []struct {
lat float64
}{
{-90.0},
{0.0},
{1.0},
{90.0},
}
for _, test := range tests {
s := scaleLat(test.lat)
lat := unscaleLat(s)
if compareGeo(test.lat, lat) != 0 {
t.Errorf("expected %.16f, got %.16f, scaled was %d", test.lat, lat, s)
}
}
}
func TestRectFromPointDistance(t *testing.T) {
// at the equator 1 degree of latitude is about 110567 meters
_, upperLeftLat, _, lowerRightLat, err := RectFromPointDistance(0, 0, 110567)
if err != nil {
t.Fatal(err)
}
if math.Abs(upperLeftLat-1) > 1e-2 {
t.Errorf("expected bounding box upper left lat to be almost 1, got %f", upperLeftLat)
}
if math.Abs(lowerRightLat+1) > 1e-2 {
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)
}
}
}

111
geo/geohash.go Normal file
View File

@@ -0,0 +1,111 @@
// Copyright (c) 2019 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.
// This implementation is inspired from the geohash-js
// ref: https://github.com/davetroy/geohash-js
package geo
// encoding encapsulates an encoding defined by a given base32 alphabet.
type encoding struct {
enc string
dec [256]byte
}
// newEncoding constructs a new encoding defined by the given alphabet,
// which must be a 32-byte string.
func newEncoding(encoder string) *encoding {
e := new(encoding)
e.enc = encoder
for i := 0; i < len(e.dec); i++ {
e.dec[i] = 0xff
}
for i := 0; i < len(encoder); i++ {
e.dec[encoder[i]] = byte(i)
}
return e
}
// base32encoding with the Geohash alphabet.
var base32encoding = newEncoding("0123456789bcdefghjkmnpqrstuvwxyz")
var masks = []uint64{16, 8, 4, 2, 1}
// DecodeGeoHash decodes the string geohash faster with
// higher precision. This api is in experimental phase.
func DecodeGeoHash(geoHash string) (float64, float64) {
even := true
lat := []float64{-90.0, 90.0}
lon := []float64{-180.0, 180.0}
for i := 0; i < len(geoHash); i++ {
cd := uint64(base32encoding.dec[geoHash[i]])
for j := 0; j < 5; j++ {
if even {
if cd&masks[j] > 0 {
lon[0] = (lon[0] + lon[1]) / 2
} else {
lon[1] = (lon[0] + lon[1]) / 2
}
} else {
if cd&masks[j] > 0 {
lat[0] = (lat[0] + lat[1]) / 2
} else {
lat[1] = (lat[0] + lat[1]) / 2
}
}
even = !even
}
}
return (lat[0] + lat[1]) / 2, (lon[0] + lon[1]) / 2
}
func EncodeGeoHash(lat, lon float64) string {
even := true
lats := []float64{-90.0, 90.0}
lons := []float64{-180.0, 180.0}
precision := 12
var ch, bit uint64
var geoHash string
for len(geoHash) < precision {
if even {
mid := (lons[0] + lons[1]) / 2
if lon > mid {
ch |= masks[bit]
lons[0] = mid
} else {
lons[1] = mid
}
} else {
mid := (lats[0] + lats[1]) / 2
if lat > mid {
ch |= masks[bit]
lats[0] = mid
} else {
lats[1] = mid
}
}
even = !even
if bit < 4 {
bit++
} else {
geoHash += string(base32encoding.enc[ch])
ch = 0
bit = 0
}
}
return geoHash
}

64
geo/geohash_test.go Normal file
View File

@@ -0,0 +1,64 @@
// Copyright (c) 2019 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 (
"strings"
"testing"
)
func TestDecodeGeoHash(t *testing.T) {
tests := []struct {
hash string
lon float64
lat float64
}{
{"d3hn3", -73.059082, 6.745605}, // -73.05908203, 6.74560547 as per http://geohash.co/
{"u4pru", 10.393066, 57.634277}, // 10.39306641, 57.63427734
{"u4pruy", 10.409546, 57.648010}, // 10.40954590, 57.64801025
{"u4pruyd", 10.407486, 57.648697}, // 10.40748596, 57.64869690
{"u4pruydqqvj", 10.40744, 57.64911}, // 10.40743969, 57.64911063
}
for _, test := range tests {
lat, lon := DecodeGeoHash(test.hash)
if compareGeo(test.lon, lon) != 0 {
t.Errorf("expected lon %f, got %f, hash %s", test.lon, lon, test.hash)
}
if compareGeo(test.lat, lat) != 0 {
t.Errorf("expected lat %f, got %f, hash %s", test.lat, lat, test.hash)
}
}
}
func TestEncodeGeoHash(t *testing.T) {
tests := []struct {
lon float64
lat float64
hash string
}{
{2.29449034, 48.85841131, "u09tunquc"},
{76.491540, 10.060349, "t9y3hx7my0fp"},
}
for _, test := range tests {
hash := EncodeGeoHash(test.lat, test.lon)
if !strings.HasPrefix(hash, test.hash) {
t.Errorf("expected hash %s, got %s", test.hash, hash)
}
}
}

465
geo/parse.go Normal file
View File

@@ -0,0 +1,465 @@
// 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 (
"reflect"
"strconv"
"strings"
"github.com/blevesearch/bleve/v2/util"
"github.com/blevesearch/geo/geojson"
)
// ExtractGeoPoint takes an arbitrary interface{} and tries it's best to
// interpret it is as geo point. Supported formats:
// Container:
// slice length 2 (GeoJSON)
//
// first element lon, second element lat
//
// string (coordinates separated by comma, or a geohash)
//
// first element lat, second element lon
//
// 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)
if !thingVal.IsValid() {
return lon, lat, false
}
thingTyp := thingVal.Type()
// is it a slice
if thingVal.Kind() == reflect.Slice {
// must be length 2
if thingVal.Len() == 2 {
first := thingVal.Index(0)
if first.CanInterface() {
firstVal := first.Interface()
lon, foundLon = util.ExtractNumericValFloat64(firstVal)
}
second := thingVal.Index(1)
if second.CanInterface() {
secondVal := second.Interface()
lat, foundLat = util.ExtractNumericValFloat64(secondVal)
}
}
}
// is it a string
if thingVal.Kind() == reflect.String {
geoStr := thingVal.Interface().(string)
if strings.Contains(geoStr, ",") {
// geo point with coordinates split by comma
points := strings.Split(geoStr, ",")
for i, point := range points {
// trim any leading or trailing white spaces
points[i] = strings.TrimSpace(point)
}
if len(points) == 2 {
var err error
lat, err = strconv.ParseFloat(points[0], 64)
if err == nil {
foundLat = true
}
lon, err = strconv.ParseFloat(points[1], 64)
if err == nil {
foundLon = true
}
}
} else {
// geohash
if len(geoStr) <= geoHashMaxLength {
lat, lon = DecodeGeoHash(geoStr)
foundLat = true
foundLon = true
}
}
}
// is it a map
if l, ok := thing.(map[string]interface{}); ok {
if lval, ok := l["lon"]; ok {
lon, foundLon = util.ExtractNumericValFloat64(lval)
} else if lval, ok := l["lng"]; ok {
lon, foundLon = util.ExtractNumericValFloat64(lval)
}
if lval, ok := l["lat"]; ok {
lat, foundLat = util.ExtractNumericValFloat64(lval)
}
}
// now try reflection on struct fields
if thingVal.Kind() == reflect.Struct {
for i := 0; i < thingVal.NumField(); i++ {
fieldName := thingTyp.Field(i).Name
if strings.HasPrefix(strings.ToLower(fieldName), "lon") {
if thingVal.Field(i).CanInterface() {
fieldVal := thingVal.Field(i).Interface()
lon, foundLon = util.ExtractNumericValFloat64(fieldVal)
}
}
if strings.HasPrefix(strings.ToLower(fieldName), "lng") {
if thingVal.Field(i).CanInterface() {
fieldVal := thingVal.Field(i).Interface()
lon, foundLon = util.ExtractNumericValFloat64(fieldVal)
}
}
if strings.HasPrefix(strings.ToLower(fieldName), "lat") {
if thingVal.Field(i).CanInterface() {
fieldVal := thingVal.Field(i).Interface()
lat, foundLat = util.ExtractNumericValFloat64(fieldVal)
}
}
}
}
// last hope, some interfaces
// lon
if l, ok := thing.(loner); ok {
lon = l.Lon()
foundLon = true
} else if l, ok := thing.(lnger); ok {
lon = l.Lng()
foundLon = true
}
// lat
if l, ok := thing.(later); ok {
lat = l.Lat()
foundLat = true
}
return lon, lat, foundLon && foundLat
}
// various support interfaces which can be used to find lat/lon
type loner interface {
Lon() float64
}
type later interface {
Lat() float64
}
type lnger interface {
Lng() float64
}
// GlueBytes primarily for quicker filtering of docvalues
// during the filtering phase.
var GlueBytes = []byte("##")
var GlueBytesOffset = len(GlueBytes)
func extractCoordinates(thing interface{}) []float64 {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil
}
if thingVal.Kind() == reflect.Slice {
// must be length 2
if thingVal.Len() == 2 {
var foundLon, foundLat bool
var lon, lat float64
first := thingVal.Index(0)
if first.CanInterface() {
firstVal := first.Interface()
lon, foundLon = util.ExtractNumericValFloat64(firstVal)
}
second := thingVal.Index(1)
if second.CanInterface() {
secondVal := second.Interface()
lat, foundLat = util.ExtractNumericValFloat64(secondVal)
}
if !foundLon || !foundLat {
return nil
}
return []float64{lon, lat}
}
}
return nil
}
func extract2DCoordinates(thing interface{}) [][]float64 {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil
}
rv := make([][]float64, 0, 8)
if thingVal.Kind() == reflect.Slice {
for j := 0; j < thingVal.Len(); j++ {
edges := thingVal.Index(j).Interface()
if es, ok := edges.([]interface{}); ok {
v := extractCoordinates(es)
if len(v) == 2 {
rv = append(rv, v)
}
}
}
return rv
}
return nil
}
func extract3DCoordinates(thing interface{}) (c [][][]float64) {
coords := reflect.ValueOf(thing)
if !coords.IsValid() {
return nil
}
if coords.Kind() == reflect.Slice {
for i := 0; i < coords.Len(); i++ {
vals := coords.Index(i)
edges := vals.Interface()
if es, ok := edges.([]interface{}); ok {
loop := extract2DCoordinates(es)
if len(loop) > 0 {
c = append(c, loop)
}
}
}
}
return c
}
func extract4DCoordinates(thing interface{}) (rv [][][][]float64) {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil
}
if thingVal.Kind() == reflect.Slice {
for j := 0; j < thingVal.Len(); j++ {
c := extract3DCoordinates(thingVal.Index(j).Interface())
rv = append(rv, c)
}
}
return rv
}
func ParseGeoShapeField(thing interface{}) (interface{}, string, error) {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil, "", nil
}
var shape string
var coordValue interface{}
if thingVal.Kind() == reflect.Map {
iter := thingVal.MapRange()
for iter.Next() {
if iter.Key().String() == "type" {
shape = iter.Value().Interface().(string)
continue
}
if iter.Key().String() == "coordinates" {
coordValue = iter.Value().Interface()
}
}
}
return coordValue, strings.ToLower(shape), nil
}
func extractGeoShape(thing interface{}) (*geojson.GeoShape, bool) {
coordValue, typ, err := ParseGeoShapeField(thing)
if err != nil {
return nil, false
}
if typ == CircleType {
return ExtractCircle(thing)
}
return ExtractGeoShapeCoordinates(coordValue, typ)
}
// ExtractGeometryCollection takes an interface{} and tries it's best to
// interpret all the member geojson shapes within it.
func ExtractGeometryCollection(thing interface{}) ([]*geojson.GeoShape, bool) {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil, false
}
var rv []*geojson.GeoShape
var f bool
if thingVal.Kind() == reflect.Map {
iter := thingVal.MapRange()
for iter.Next() {
if iter.Key().String() == "type" {
continue
}
if iter.Key().String() == "geometries" {
collection := iter.Value().Interface()
items := reflect.ValueOf(collection)
for j := 0; j < items.Len(); j++ {
shape, found := extractGeoShape(items.Index(j).Interface())
if found {
f = found
rv = append(rv, shape)
}
}
}
}
}
return rv, f
}
// ExtractCircle takes an interface{} and tries it's best to
// interpret the center point coordinates and the radius for a
// given circle shape.
func ExtractCircle(thing interface{}) (*geojson.GeoShape, bool) {
thingVal := reflect.ValueOf(thing)
if !thingVal.IsValid() {
return nil, false
}
rv := &geojson.GeoShape{
Type: CircleType,
Center: make([]float64, 0, 2),
}
if thingVal.Kind() == reflect.Map {
iter := thingVal.MapRange()
for iter.Next() {
if iter.Key().String() == "radius" {
rv.Radius = iter.Value().Interface().(string)
continue
}
if iter.Key().String() == "coordinates" {
lng, lat, found := ExtractGeoPoint(iter.Value().Interface())
if !found {
return nil, false
}
rv.Center = append(rv.Center, lng, lat)
}
}
}
return rv, true
}
// ExtractGeoShapeCoordinates takes an interface{} and tries it's best to
// interpret the coordinates for any of the given geoshape typ like
// a point, multipoint, linestring, multilinestring, polygon, multipolygon,
func ExtractGeoShapeCoordinates(coordValue interface{},
typ string) (*geojson.GeoShape, bool) {
rv := &geojson.GeoShape{
Type: typ,
}
if typ == PointType {
point := extractCoordinates(coordValue)
// ignore the contents with invalid entry.
if len(point) < 2 {
return nil, false
}
rv.Coordinates = [][][][]float64{{{point}}}
return rv, true
}
if typ == MultiPointType || typ == LineStringType ||
typ == EnvelopeType {
coords := extract2DCoordinates(coordValue)
// ignore the contents with invalid entry.
if len(coords) == 0 {
return nil, false
}
if typ == EnvelopeType && len(coords) != 2 {
return nil, false
}
if typ == LineStringType && len(coords) < 2 {
return nil, false
}
rv.Coordinates = [][][][]float64{{coords}}
return rv, true
}
if typ == PolygonType || typ == MultiLineStringType {
coords := extract3DCoordinates(coordValue)
// ignore the contents with invalid entry.
if len(coords) == 0 {
return nil, false
}
if typ == PolygonType && len(coords[0]) < 3 ||
typ == MultiLineStringType && len(coords[0]) < 2 {
return nil, false
}
rv.Coordinates = [][][][]float64{coords}
return rv, true
}
if typ == MultiPolygonType {
coords := extract4DCoordinates(coordValue)
// ignore the contents with invalid entry.
if len(coords) == 0 || len(coords[0]) == 0 {
return nil, false
}
if len(coords[0][0]) < 3 {
return nil, false
}
rv.Coordinates = coords
return rv, true
}
return rv, false
}

478
geo/parse_test.go Normal file
View File

@@ -0,0 +1,478 @@
// 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 (
"encoding/json"
"reflect"
"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 alternate
{
in: struct {
Lng float64
Lat float64
}{
Lng: 3.0,
Lat: 7.5,
},
lon: 3.0,
lat: 7.5,
success: true,
},
// test going through interface
{
in: &s11{
lon: 4.0,
lat: 6.9,
},
lon: 4.0,
lat: 6.9,
success: true,
},
// test going through interface with lng variant
{
in: &s12{
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,
},
// values are nil (not supported)
{
in: map[string]interface{}{
"lat": nil,
"lon": nil,
},
lon: 0,
lat: 0,
success: false,
},
// input is nil
{
in: nil,
lon: 0,
lat: 0,
success: false,
},
}
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 s11 struct {
lon float64
lat float64
}
func (s *s11) Lon() float64 {
return s.lon
}
func (s *s11) Lat() float64 {
return s.lat
}
type s12 struct {
lng float64
lat float64
}
func (s *s12) Lng() float64 {
return s.lng
}
func (s *s12) Lat() float64 {
return s.lat
}
func TestExtractGeoShape(t *testing.T) {
tests := []struct {
in interface{}
resTyp string
coordinates [][][][]float64
center []float64
success bool
}{
// valid point slice
{
in: map[string]interface{}{
"coordinates": []interface{}{3.4, 5.9},
"type": "Point",
},
resTyp: "point",
coordinates: [][][][]float64{{{{3.4, 5.9}}}},
success: true,
},
// invalid point slice
{
in: map[string]interface{}{
"coordinates": []interface{}{3.4},
"type": "point"},
resTyp: "point",
coordinates: nil,
success: false,
},
// valid multipoint slice containing single point
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 5.9}},
"type": "multipoint"},
resTyp: "multipoint",
coordinates: [][][][]float64{{{{3.4, 5.9}}}},
success: true,
},
// valid multipoint slice
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 5.9}, {6.7, 9.8}},
"type": "multipoint"},
resTyp: "multipoint",
coordinates: [][][][]float64{{{{3.4, 5.9}, {6.7, 9.8}}}},
success: true,
},
// valid multipoint slice containing one invalid entry
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 5.9}, {6.7}},
"type": "multipoint"},
resTyp: "multipoint",
coordinates: [][][][]float64{{{{3.4, 5.9}}}},
success: true,
},
// invalid multipoint slice
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4}},
"type": "multipoint"},
resTyp: "multipoint",
coordinates: nil,
success: false,
},
// valid linestring slice
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}},
"type": "linestring"},
resTyp: "linestring",
coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}},
success: true,
},
// valid linestring slice
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}},
"type": "linestring"},
resTyp: "linestring",
coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}}}},
success: true,
},
// invalid linestring slice with single entry
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}},
"type": "linestring"},
resTyp: "linestring",
coordinates: nil,
success: false,
},
// invalid linestring slice with wrong parenthesis
{
in: map[string]interface{}{
"coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}},
"type": "linestring"},
resTyp: "linestring",
coordinates: nil,
success: false,
},
// valid envelope
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}},
"type": "envelope"},
resTyp: "envelope",
coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}},
success: true,
},
// invalid envelope
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}},
"type": "envelope"},
resTyp: "envelope",
coordinates: nil,
success: false,
},
// invalid envelope
{
in: map[string]interface{}{
"coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}},
"type": "envelope"},
resTyp: "envelope",
coordinates: nil,
success: false,
},
// invalid envelope with >2 vertices
{
in: map[string]interface{}{
"coordinates": [][]interface{}{{3.4, 4.4}, {5.6, 6.4}, {7.4, 7.4}},
"type": "envelope"},
resTyp: "envelope",
coordinates: nil,
success: false,
},
// valid circle
{
in: map[string]interface{}{
"coordinates": []interface{}{4.4, 5.0},
"radius": "200m",
"type": "circle"},
resTyp: "circle",
center: []float64{4.4, 5.0},
success: true,
},
// invalid circle
{
in: map[string]interface{}{
"coordinates": []interface{}{4.4},
"radius": "200m",
"type": "circle"},
resTyp: "circle",
success: false,
},
}
for _, test := range tests {
res, success := extractGeoShape(test.in)
if success != test.success {
t.Errorf("expected extract geo point: %t, got: %t for: %v", test.success, success, test.in)
}
if success && res.Type != test.resTyp {
t.Errorf("expected shape type: %v, got: %v for input: %v", test.resTyp, res.Type, test.in)
}
if success && !reflect.DeepEqual(test.coordinates, res.Coordinates) {
t.Errorf("expected result %+v, got %+v for %v", test.coordinates, res.Coordinates, test.in)
}
if success && test.center != nil && !reflect.DeepEqual(test.center, res.Center) {
t.Errorf("expected center %+v, got %+v for %v", test.center, res.Center, test.in)
}
}
}
func TestExtractGeoShapeCoordinates(t *testing.T) {
tests := []struct {
x []byte
typ string
expectOK bool
}{
{
x: []byte(`[
[
[77.58894681930542,12.976498523818783],
[77.58677959442139,12.974533005048169],
[77.58894681930542,12.976498523818783]
]
]`),
typ: PolygonType,
expectOK: true,
},
{ // Invalid construct, but handled
x: []byte(`[
[
{"lon":77.58894681930542,"lat":12.976498523818783},
{"lon":77.58677959442139,"lat":12.974533005048169},
{"lon":77.58894681930542,"lat":12.976498523818783}
]
]`),
typ: PolygonType,
expectOK: false,
},
{ // Invalid construct causes panic (within extract3DCoordinates), fix MB-65807
x: []byte(`{
"coordinates": [
[77.58894681930542,12.976498523818783],
[77.58677959442139,12.974533005048169],
[77.58894681930542,12.976498523818783]
]
}`),
typ: PolygonType,
expectOK: false,
},
{
x: []byte(`[
[
[
[-0.163421630859375,51.531600743186644],
[-0.15277862548828125,51.52455221546295],
[-0.15895843505859375,51.53693981046689],
[-0.163421630859375,51.531600743186644]
]
],
[
[
[-0.1902008056640625,51.5091698216777],
[-0.1599884033203125,51.51322956905176],
[-0.1902008056640625,51.5091698216777]
]
]
]`),
typ: MultiPolygonType,
expectOK: true,
},
{ // Invalid construct causes panic (within extract3DCoordinates), fix MB-65807
x: []byte(`[
{
"coordinates": [
[-0.163421630859375,51.531600743186644],
[-0.15277862548828125,51.52455221546295],
[-0.15895843505859375,51.53693981046689],
[-0.163421630859375,51.531600743186644]
]
},
{
"coordinates": [
[-0.1902008056640625,51.5091698216777],
[-0.1599884033203125,51.51322956905176],
[-0.1902008056640625,51.5091698216777]
]
}
]`),
typ: MultiPolygonType,
expectOK: false,
},
}
for i := range tests {
var x interface{}
if err := json.Unmarshal(tests[i].x, &x); err != nil {
t.Fatalf("[%d] JSON err: %v", i+1, err)
}
res, ok := ExtractGeoShapeCoordinates(x, tests[i].typ)
if ok != tests[i].expectOK {
t.Errorf("[%d] expected ok %t, got %t", i+1, tests[i].expectOK, ok)
}
if ok && res.Type != tests[i].typ {
t.Errorf("[%d] expected type %s, got %s", i+1, tests[i].typ, res.Type)
}
}
}

59
geo/sloppy.go Normal file
View File

@@ -0,0 +1,59 @@
// 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"
)
var earthDiameterPerLatitude []float64
const (
radiusTabsSize = (1 << 10) + 1
radiusDelta = (math.Pi / 2) / (radiusTabsSize - 1)
radiusIndexer = 1 / radiusDelta
)
func init() {
// initializes the tables used for the sloppy math functions
// earth radius
a := 6378137.0
b := 6356752.31420
a2 := a * a
b2 := b * b
earthDiameterPerLatitude = make([]float64, radiusTabsSize)
earthDiameterPerLatitude[0] = 2.0 * a / 1000
earthDiameterPerLatitude[radiusTabsSize-1] = 2.0 * b / 1000
for i := 1; i < radiusTabsSize-1; i++ {
lat := math.Pi * float64(i) / (2*radiusTabsSize - 1)
one := math.Pow(a2*math.Cos(lat), 2)
two := math.Pow(b2*math.Sin(lat), 2)
three := math.Pow(float64(a)*math.Cos(lat), 2)
four := math.Pow(b*math.Sin(lat), 2)
radius := math.Sqrt((one + two) / (three + four))
earthDiameterPerLatitude[i] = 2 * radius / 1000
}
}
// earthDiameter returns an estimation of the earth's diameter at the specified
// latitude in kilometers
func earthDiameter(lat float64) float64 {
index := math.Mod(math.Abs(lat)*radiusIndexer+0.5, float64(len(earthDiameterPerLatitude)))
if math.IsNaN(index) {
return 0
}
return earthDiameterPerLatitude[int(index)]
}