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
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:
312
geo/README.md
Normal file
312
geo/README.md
Normal 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.
|
||||
43
geo/benchmark_geohash_test.go
Normal file
43
geo/benchmark_geohash_test.go
Normal 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
210
geo/geo.go
Normal 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
98
geo/geo_dist.go
Normal 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, ¢im, &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
120
geo/geo_dist_test.go
Normal 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
523
geo/geo_s2plugin_impl.go
Normal 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
183
geo/geo_test.go
Normal 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
111
geo/geohash.go
Normal 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
64
geo/geohash_test.go
Normal 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
465
geo/parse.go
Normal 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
478
geo/parse_test.go
Normal 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
59
geo/sloppy.go
Normal 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)]
|
||||
}
|
||||
Reference in New Issue
Block a user