From a6afb237d83060c6ce1264c0101012c3867b17df Mon Sep 17 00:00:00 2001 From: hmarques Date: Sat, 16 Mar 2019 16:04:59 -0300 Subject: [PATCH 1/2] Implement Geo Shape Query Based on the elasticsearch and GeoJSON specification, this PR try to solve the #939 supporting the follow types : * Circle * Envelope * Linestring * MultiLineString * MultiPoint * MultiPolygon * Point * Polygon Any suggestion would be appreciated. Change-Id: If7e6f59a3833da665e31e7ec179951780d82f9c2 --- geo/circle.go | 37 ++++ geo/envelope.go | 32 ++++ geo/exampledata_test.go | 156 +++++++++++++++++ geo/geometry.go | 29 ++++ geo/geometry_test.go | 285 +++++++++++++++++++++++++++++++ geo/geometrycollection.go | 95 +++++++++++ geo/linestring.go | 31 ++++ geo/multilinestring.go | 31 ++++ geo/multipoint.go | 31 ++++ geo/multipolygon.go | 31 ++++ geo/options.go | 76 +++++++++ geo/point.go | 31 ++++ geo/polygon.go | 31 ++++ geo/shape.go | 172 +++++++++++++++++++ search_queries_geo_shape.go | 177 +++++++++++++++++++ search_queries_geo_shape_test.go | 217 +++++++++++++++++++++++ 16 files changed, 1462 insertions(+) create mode 100644 geo/circle.go create mode 100644 geo/envelope.go create mode 100644 geo/exampledata_test.go create mode 100644 geo/geometry.go create mode 100644 geo/geometry_test.go create mode 100644 geo/geometrycollection.go create mode 100644 geo/linestring.go create mode 100644 geo/multilinestring.go create mode 100644 geo/multipoint.go create mode 100644 geo/multipolygon.go create mode 100644 geo/options.go create mode 100644 geo/point.go create mode 100644 geo/polygon.go create mode 100644 geo/shape.go create mode 100644 search_queries_geo_shape.go create mode 100644 search_queries_geo_shape_test.go diff --git a/geo/circle.go b/geo/circle.go new file mode 100644 index 000000000..a87611d98 --- /dev/null +++ b/geo/circle.go @@ -0,0 +1,37 @@ +package geo + +import ( + "encoding/json" +) + +// Circle specified by a center point and radius with units, +// which default to meters. +type Circle struct { + Type string `json:"type"` + Radius string `json:"radius"` + Coordinates []float64 `json:"coordinates"` +} + +// NewCircle creates and initializes a new Circle. +func NewCircle(radius string, coordinates []float64) *Circle { + return &Circle{ + Type: TypeCircle, + Radius: radius, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the circle data into a GeoJSON geometry. +func (m *Circle) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *Circle) decode(raw *rawGeometry) error { + m.Type = raw.Type + m.Radius = raw.Radius + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/envelope.go b/geo/envelope.go new file mode 100644 index 000000000..891d5d0d0 --- /dev/null +++ b/geo/envelope.go @@ -0,0 +1,32 @@ +package geo + +import "encoding/json" + +// Envelope consists of coordinates for upper left and lower right points of the shape +// to represent a bounding rectangle. +type Envelope struct { + Type string `json:"type"` + Coordinates [][]float64 `json:"coordinates"` +} + +// NewEnvelope creates and initializes a new Rectangle. +func NewEnvelope(coordinates [][]float64) *Envelope { + return &Envelope{ + Type: TypeEnvelope, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the envelope data into a GeoJSON geometry. +func (m *Envelope) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *Envelope) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/exampledata_test.go b/geo/exampledata_test.go new file mode 100644 index 000000000..128c0f876 --- /dev/null +++ b/geo/exampledata_test.go @@ -0,0 +1,156 @@ +package geo + +// Examples from RFC7946 +// https://tools.ietf.org/html/rfc7946 + +var point = []byte(` +{ + "type":"point", + "coordinates":[100.0, 0.0] +} +`) + +var multiPoint = []byte(` +{ + "type":"multipoint", + "coordinates":[ + [100.0, 0.0], + [101.0, 1.0] + ] +} +`) + +var lineString = []byte(` +{ + "type":"linestring", + "coordinates":[ + [100.0, 0.0], + [101.0, 1.0] + ] +} +`) + +var multiLineString = []byte(` + +{ + "type":"multilinestring", + "coordinates":[ + [ + [100.0, 0.0], + [101.0, 1.0] + ], + [ + [102.0, 2.0], + [103.0, 3.0] + ] + ] +} +`) + +var polygon = []byte(` +{ + "type":"polygon", + "coordinates":[ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] +} +`) + +var polygonWithHoles = []byte(` +{ + "type":"polygon", + "coordinates":[ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.2, 0.2], + [100.8, 0.2], + [100.8, 0.8], + [100.2, 0.8], + [100.2, 0.2] + ] + ] +} + +`) + +var multiPolygon = []byte(` +{ + "type":"multipolygon", + "coordinates":[ + [ + [ + [102.0, 2.0], + [103.0, 2.0], + [103.0, 3.0], + [102.0, 3.0], + [102.0, 2.0] + ] + ], + [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.2, 0.2], + [100.8, 0.2], + [100.8, 0.8], + [100.2, 0.8], + [100.2, 0.2] + ] + ] + ] +} +`) + +var geometryCollection = []byte(` +{ + "type":"geometrycollection", + "geometries":[ + { + "type":"point", + "coordinates":[100.0, 0.0] + }, + { + "type":"linestring", + "coordinates":[ + [101.0, 0.0], + [102.0, 1.0] + ] + } + ] +} +`) + +var envelope = []byte(` +{ + "type":"envelope", + "coordinates":[ + [100.0, 1.0], + [101.0, 0.0] + ] +} +`) + +var circle = []byte(` +{ + "type":"circle", + "radius":"25m", + "coordinates":[-109.874838, 44.439550] +} +`) diff --git a/geo/geometry.go b/geo/geometry.go new file mode 100644 index 000000000..71aeb631f --- /dev/null +++ b/geo/geometry.go @@ -0,0 +1,29 @@ +package geo + +import ( + "encoding/json" +) + +// The geometry types supported by Elasticsearch. +// +// For more details, see: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#spatial-strategy +const ( + TypePoint = "point" + TypeMultiPoint = "multipoint" + TypeLineString = "linestring" + TypeMultiLineString = "multilinestring" + TypePolygon = "polygon" + TypeMultiPolygon = "multipolygon" + TypeGeometryCollection = "geometrycollection" + TypeEnvelope = "envelope" + TypeCircle = "circle" +) + +// rawGeometry holds generic data used to unmarshal GeoJSON information. +type rawGeometry struct { + Type string `json:"type"` + Coordinates json.RawMessage `json:"coordinates"` + Geometries []rawGeometry `json:"geometries"` + Radius string `json:"radius,omitempty"` +} diff --git a/geo/geometry_test.go b/geo/geometry_test.go new file mode 100644 index 000000000..9c14e0550 --- /dev/null +++ b/geo/geometry_test.go @@ -0,0 +1,285 @@ +package geo + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPoint(t *testing.T) { + var ( + expected = NewPoint([]float64{100.0, 0.0}) + actual = &Point{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Point", func(t *testing.T) { + err := json.Unmarshal(point, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Point", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(point), string(actual)) + }) +} + +func TestMultiPoint(t *testing.T) { + var ( + expected = NewMultiPoint([][]float64{ + {100.0, 0.0}, {101.0, 1.0}, + }) + actual = &MultiPoint{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal MultiPoint", func(t *testing.T) { + err := json.Unmarshal(multiPoint, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal MultiPoint", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(multiPoint), string(actual)) + }) +} + +func TestLineString(t *testing.T) { + var ( + expected = NewLineString([][]float64{ + {100.0, 0.0}, {101.0, 1.0}, + }) + actual = &LineString{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal LineString", func(t *testing.T) { + err := json.Unmarshal(lineString, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal LineString", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(lineString), string(actual)) + }) +} + +func TestMultiLineString(t *testing.T) { + var ( + expected = NewMultiLineString([][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 1.0}, + }, + [][]float64{ + {102.0, 2.0}, {103.0, 3.0}, + }, + }) + actual = &MultiLineString{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal MultiLineString", func(t *testing.T) { + err := json.Unmarshal(multiLineString, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal MultiLineString", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(multiLineString), string(actual)) + }) +} + +func TestPolygonNoHoles(t *testing.T) { + var ( + expected = NewPolygon([][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + }) + actual = &Polygon{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Polygon", func(t *testing.T) { + err := json.Unmarshal(polygon, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Polygon", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(polygon), string(actual)) + }) +} + +func TestPolygonWithHoles(t *testing.T) { + var ( + expected = NewPolygon([][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + [][]float64{ + {100.2, 0.2}, {100.8, 0.2}, {100.8, 0.8}, {100.2, 0.8}, {100.2, 0.2}, + }, + }) + actual = &Polygon{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Polygon With Holes", func(t *testing.T) { + err := json.Unmarshal(polygonWithHoles, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Polygon With Holes", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(polygonWithHoles), string(actual)) + }) +} + +func TestMultiPolygon(t *testing.T) { + var ( + expected = NewMultiPolygon([][][][]float64{ + { + [][]float64{ + {102.0, 2.0}, {103.0, 2.0}, {103.0, 3.0}, {102.0, 3.0}, {102.0, 2.0}, + }, + }, + { + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + [][]float64{ + {100.2, 0.2}, {100.8, 0.2}, {100.8, 0.8}, {100.2, 0.8}, {100.2, 0.2}, + }, + }, + }) + actual = &MultiPolygon{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal MultiPolygon", func(t *testing.T) { + err := json.Unmarshal(multiPolygon, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal MultiPolygon", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(multiPolygon), string(actual)) + }) +} + +func TestGeometryCollection(t *testing.T) { + var ( + expected = NewGeometryCollection([]interface{}{ + &Point{ + Type: "point", + Coordinates: []float64{100.0, 0.0}, + }, + &LineString{ + Type: "linestring", + Coordinates: [][]float64{ + {101.0, 0.0}, {102.0, 1.0}, + }, + }, + }) + actual = &GeometryCollection{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal GeometryCollection", func(t *testing.T) { + err := json.Unmarshal(geometryCollection, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal GeometryCollection", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(geometryCollection), string(actual)) + }) +} + +func TestShape(t *testing.T) { + var ( + expected = &Shape{ + Type: "polygon", + Polygon: NewPolygon([][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + }), + } + actual = &Shape{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Shape", func(t *testing.T) { + err := json.Unmarshal(polygon, actual) + assert.NoError(err) + assert.True(actual.IsPolygon()) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Shape", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(polygon), string(actual)) + }) +} + +func TestEnvelope(t *testing.T) { + var ( + expected = NewEnvelope([][]float64{ + {100.0, 1.0}, {101.0, 0.0}, + }) + actual = &Envelope{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Envelope", func(t *testing.T) { + err := json.Unmarshal(envelope, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Envelope", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(envelope), string(actual)) + }) +} + +func TestCircle(t *testing.T) { + var ( + expected = NewCircle("25m", []float64{-109.874838, 44.439550}) + actual = &Circle{} + ) + assert := require.New(t) + + t.Run("Test Unmarshal Circle", func(t *testing.T) { + err := json.Unmarshal(circle, actual) + assert.NoError(err) + assert.Equal(actual, expected) + }) + + t.Run("Test Marshal Circle", func(t *testing.T) { + actual, err := json.Marshal(expected) + assert.NoError(err) + assert.JSONEq(string(circle), string(actual)) + }) +} diff --git a/geo/geometrycollection.go b/geo/geometrycollection.go new file mode 100644 index 000000000..1d043c3f0 --- /dev/null +++ b/geo/geometrycollection.go @@ -0,0 +1,95 @@ +package geo + +import ( + "encoding/json" + "fmt" +) + +// GeometryCollection is a geometry object which represents a collection of geometry objects. +type GeometryCollection struct { + Type string `json:"type"` + Geometries []interface{} `json:"geometries"` +} + +// NewGeometryCollection creates and initializes a new GeometryCollection. +func NewGeometryCollection(geometries []interface{}) *GeometryCollection { + return &GeometryCollection{ + Type: TypeGeometryCollection, + Geometries: geometries, + } +} + +// UnmarshalJSON decodes the geometry collection data into a GeoJSON geometry. +func (m *GeometryCollection) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *GeometryCollection) decode(raw *rawGeometry) error { + m.Type = raw.Type + m.Geometries = make([]interface{}, 0, len(raw.Geometries)) + for _, geometry := range raw.Geometries { + switch geometry.Type { + case TypePoint: + point := &Point{} + if err := point.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, point) + case TypeMultiPoint: + multipoint := &MultiPoint{} + if err := multipoint.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, multipoint) + case TypeLineString: + lineString := &LineString{} + if err := lineString.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, lineString) + case TypeMultiLineString: + multiLine := &MultiLineString{} + if err := multiLine.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, multiLine) + case TypePolygon: + polygon := &Polygon{} + if err := polygon.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, polygon) + case TypeMultiPolygon: + multiPolygon := &MultiPolygon{} + if err := multiPolygon.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, multiPolygon) + case TypeGeometryCollection: + geometryCollection := &GeometryCollection{} + if err := geometryCollection.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, geometryCollection) + case TypeEnvelope: + envelope := &Envelope{} + if err := envelope.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, envelope) + case TypeCircle: + circle := &Circle{} + if err := circle.decode(&geometry); err != nil { + return err + } + m.Geometries = append(m.Geometries, circle) + default: + return fmt.Errorf("geo: unknown type `%s`", m.Type) + } + } + return nil +} diff --git a/geo/linestring.go b/geo/linestring.go new file mode 100644 index 000000000..e4f4383fe --- /dev/null +++ b/geo/linestring.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// LineString is an object that must be an array of two or more positions. +type LineString struct { + Type string `json:"type"` + Coordinates [][]float64 `json:"coordinates"` +} + +// NewLineString creates and initializes a new LineString. +func NewLineString(coordinates [][]float64) *LineString { + return &LineString{ + Type: TypeLineString, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the linestring data into a GeoJSON geometry. +func (m *LineString) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *LineString) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/multilinestring.go b/geo/multilinestring.go new file mode 100644 index 000000000..d8812c0f0 --- /dev/null +++ b/geo/multilinestring.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// MultiLineString is an object that must be an array of LineString coordinate arrays. +type MultiLineString struct { + Type string `json:"type"` + Coordinates [][][]float64 `json:"coordinates"` +} + +// NewMultiLineString creates and initializes a new MultiLineString. +func NewMultiLineString(coordinates [][][]float64) *MultiLineString { + return &MultiLineString{ + Type: TypeMultiLineString, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the multi linestring data into a GeoJSON geometry. +func (m *MultiLineString) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *MultiLineString) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/multipoint.go b/geo/multipoint.go new file mode 100644 index 000000000..2ff19a31e --- /dev/null +++ b/geo/multipoint.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// MultiPoint is an object that must be an array of positions. +type MultiPoint struct { + Type string `json:"type"` + Coordinates [][]float64 `json:"coordinates"` +} + +// NewMultiPoint creates and initializes a new MultiPoint. +func NewMultiPoint(coordinates [][]float64) *MultiPoint { + return &MultiPoint{ + Type: TypeMultiPoint, + Coordinates: [][]float64(coordinates), + } +} + +// UnmarshalJSON decodes the multipoint data into a GeoJSON geometry. +func (m *MultiPoint) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *MultiPoint) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/multipolygon.go b/geo/multipolygon.go new file mode 100644 index 000000000..5eb1f5a79 --- /dev/null +++ b/geo/multipolygon.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// MultiPolygon represents a GeoJSON object of multiple Polygons. +type MultiPolygon struct { + Type string `json:"type"` + Coordinates [][][][]float64 `json:"coordinates"` +} + +// NewMultiPolygon creates and initializes a new MultiPolygon. +func NewMultiPolygon(coordinates [][][][]float64) *MultiPolygon { + return &MultiPolygon{ + Type: TypeMultiPolygon, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the multi polygon data into a GeoJSON geometry. +func (m *MultiPolygon) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *MultiPolygon) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/options.go b/geo/options.go new file mode 100644 index 000000000..8b30b928e --- /dev/null +++ b/geo/options.go @@ -0,0 +1,76 @@ +package geo + +// Option allows to configure various aspects of Shape. +type Option func(*Shape) + +// WithPoint set the shape as Point. +func WithPoint(coordinates []float64) Option { + return func(s *Shape) { + s.Type = TypePoint + s.Point = NewPoint(coordinates) + } +} + +// WithMultiPoint set the shape as MultiPoint. +func WithMultiPoint(coordinates [][]float64) Option { + return func(s *Shape) { + s.Type = TypeMultiPoint + s.MultiPoint = NewMultiPoint(coordinates) + } +} + +// WithLineString set the shape as LineString. +func WithLineString(coordinates [][]float64) Option { + return func(s *Shape) { + s.Type = TypeLineString + s.LineString = NewLineString(coordinates) + } +} + +// WithMultiLineString set the shape as MultiLineString. +func WithMultiLineString(coordinates [][][]float64) Option { + return func(s *Shape) { + s.Type = TypeMultiLineString + s.MultiLineString = NewMultiLineString(coordinates) + } +} + +// WithPolygon set the shape as Polygon. +func WithPolygon(coordinates [][][]float64) Option { + return func(s *Shape) { + s.Type = TypePolygon + s.Polygon = NewPolygon(coordinates) + } +} + +// WithMultiPolygon set the shape as MultiPolygon. +func WithMultiPolygon(coordinates [][][][]float64) Option { + return func(s *Shape) { + s.Type = TypeMultiPolygon + s.MultiPolygon = NewMultiPolygon(coordinates) + } +} + +// WithGeometryCollection set the shape as GeometryCollection. +func WithGeometryCollection(geometries []interface{}) Option { + return func(s *Shape) { + s.Type = TypeGeometryCollection + s.GeometryCollection = NewGeometryCollection(geometries) + } +} + +// WithEnvelope set the shape as Envelope. +func WithEnvelope(coordinates [][]float64) Option { + return func(s *Shape) { + s.Type = TypeEnvelope + s.Envelope = NewEnvelope(coordinates) + } +} + +// WithCircle set the shape as Circle. +func WithCircle(radius string, coordinates []float64) Option { + return func(s *Shape) { + s.Type = TypeCircle + s.Circle = NewCircle(radius, coordinates) + } +} diff --git a/geo/point.go b/geo/point.go new file mode 100644 index 000000000..78368c5d6 --- /dev/null +++ b/geo/point.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// Point is an object that must be a single position. +type Point struct { + Type string `json:"type"` + Coordinates []float64 `json:"coordinates"` +} + +// NewPoint creates and initializes a new Point. +func NewPoint(coordinates []float64) *Point { + return &Point{ + Type: TypePoint, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the point data into a GeoJSON geometry. +func (m *Point) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *Point) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/polygon.go b/geo/polygon.go new file mode 100644 index 000000000..4555fbd5f --- /dev/null +++ b/geo/polygon.go @@ -0,0 +1,31 @@ +package geo + +import "encoding/json" + +// Polygon is an object that must be an array of LinearRing coordinate arrays. +type Polygon struct { + Type string `json:"type"` + Coordinates [][][]float64 `json:"coordinates"` +} + +// NewPolygon creates and initializes a new Polygon. +func NewPolygon(coordinates [][][]float64) *Polygon { + return &Polygon{ + Type: TypePolygon, + Coordinates: coordinates, + } +} + +// UnmarshalJSON decodes the polygon data into a GeoJSON geometry. +func (m *Polygon) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +func (m *Polygon) decode(raw *rawGeometry) error { + m.Type = raw.Type + return json.Unmarshal(raw.Coordinates, &m.Coordinates) +} diff --git a/geo/shape.go b/geo/shape.go new file mode 100644 index 000000000..4a8940b09 --- /dev/null +++ b/geo/shape.go @@ -0,0 +1,172 @@ +package geo + +import ( + "encoding/json" + "fmt" +) + +// Shape is a `generic` object used to unmarshal geometric objects. +type Shape struct { + Type string `json:"type"` + Point *Point `json:"-"` + MultiPoint *MultiPoint `json:"-"` + LineString *LineString `json:"-"` + MultiLineString *MultiLineString `json:"-"` + Polygon *Polygon `json:"-"` + MultiPolygon *MultiPolygon `json:"-"` + GeometryCollection *GeometryCollection `json:"-"` + Envelope *Envelope `json:"-"` + Circle *Circle `json:"-"` +} + +// NewShape creates and initializes a new shape based on given option. +func NewShape(opts ...Option) *Shape { + shape := &Shape{} + for _, opt := range opts { + opt(shape) + } + return shape +} + +// IsPoint returns true whether valid Point object. +func (m *Shape) IsPoint() bool { + return m.Type == TypePoint && m.Point != nil +} + +// IsMultiPoint returns true whether valid MultiPoint object. +func (m *Shape) IsMultiPoint() bool { + return m.Type == TypeMultiPoint && m.MultiPoint != nil +} + +// IsLineString returns true whether valid LineString object. +func (m *Shape) IsLineString() bool { + return m.Type == TypeLineString && m.LineString != nil +} + +// IsMultiLineString returns true whether valid MultiLineString object. +func (m *Shape) IsMultiLineString() bool { + return m.Type == TypeMultiLineString && m.MultiLineString != nil +} + +// IsPolygon returns true whether valid Polygon object. +func (m *Shape) IsPolygon() bool { + return m.Type == TypePolygon && m.Polygon != nil +} + +// IsMultiPolygon returns true whether valid MultiPolygon. +func (m *Shape) IsMultiPolygon() bool { + return m.Type == TypeMultiPolygon && m.MultiPolygon != nil +} + +// IsGeometryCollection returns true whether valid GeometryCollection. +func (m *Shape) IsGeometryCollection() bool { + return m.Type == TypeGeometryCollection && m.GeometryCollection != nil +} + +// IsEnvelope returns true whether valid Envelope. +func (m *Shape) IsEnvelope() bool { + return m.Type == TypeEnvelope && m.Envelope != nil +} + +// IsCircle returns true whether valid Circle. +func (m *Shape) IsCircle() bool { + return m.Type == TypeCircle && m.Circle != nil +} + +// UnmarshalJSON decodes the geometry data into a GeoJSON. +func (m *Shape) UnmarshalJSON(data []byte) error { + raw := &rawGeometry{} + if err := json.Unmarshal(data, raw); err != nil { + return err + } + return m.decode(raw) +} + +// MarshalJSON encodes the geomettry data into a GeoJSON. +func (m *Shape) MarshalJSON() ([]byte, error) { + switch { + case m.IsPoint(): + return json.Marshal(m.Point) + case m.IsMultiPoint(): + return json.Marshal(m.MultiPoint) + case m.IsLineString(): + return json.Marshal(m.LineString) + case m.IsMultiLineString(): + return json.Marshal(m.MultiLineString) + case m.IsPolygon(): + return json.Marshal(m.Polygon) + case m.IsMultiPolygon(): + return json.Marshal(m.MultiPolygon) + case m.IsGeometryCollection(): + return json.Marshal(m.GeometryCollection) + case m.IsEnvelope(): + return json.Marshal(m.Envelope) + case m.IsCircle(): + return json.Marshal(m.Circle) + default: + return nil, fmt.Errorf("geo: unknown type `%s`", m.Type) + } +} + +func (m *Shape) decode(raw *rawGeometry) error { + m.Type = raw.Type + switch raw.Type { + case TypePoint: + point := &Point{} + if err := point.decode(raw); err != nil { + return err + } + m.Point = point + case TypeMultiPoint: + multipoint := &MultiPoint{} + if err := multipoint.decode(raw); err != nil { + return err + } + m.MultiPoint = multipoint + case TypeLineString: + lineString := &LineString{} + if err := lineString.decode(raw); err != nil { + return err + } + m.LineString = lineString + case TypeMultiLineString: + multiLine := &MultiLineString{} + if err := multiLine.decode(raw); err != nil { + return err + } + m.MultiLineString = multiLine + case TypePolygon: + polygon := &Polygon{} + if err := polygon.decode(raw); err != nil { + return err + } + m.Polygon = polygon + case TypeMultiPolygon: + multiPolygon := &MultiPolygon{} + if err := multiPolygon.decode(raw); err != nil { + return err + } + m.MultiPolygon = multiPolygon + case TypeGeometryCollection: + geometryCollection := &GeometryCollection{} + if err := geometryCollection.decode(raw); err != nil { + return err + } + m.GeometryCollection = geometryCollection + case TypeEnvelope: + envelope := &Envelope{} + if err := envelope.decode(raw); err != nil { + return err + } + m.Envelope = envelope + case TypeCircle: + circle := &Circle{} + if err := circle.decode(raw); err != nil { + return err + } + m.Circle = circle + default: + return fmt.Errorf("geo: unknown type `%s`", m.Type) + } + return nil +} diff --git a/search_queries_geo_shape.go b/search_queries_geo_shape.go new file mode 100644 index 000000000..9f8f004c2 --- /dev/null +++ b/search_queries_geo_shape.go @@ -0,0 +1,177 @@ +// Copyright 2012-present Oliver Eilhard. All rights reserved. +// Use of this source code is governed by a MIT-license. +// See http://olivere.mit-license.org/license.txt for details. + +package elastic + +import ( + "github.com/olivere/elastic/geo" +) + +// GeoShapeQuery allows to find documents that have a shape that intersects with query shape. +// +// For more details, see: +// +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html +type GeoShapeQuery struct { + path string + relation string + queryName string + shape *geo.Shape + indexedShape *IndexedShape +} + +// Pre-Indexed Shape the query also supports using a shape which has already been +// indexed in another index and/or type. +type IndexedShape struct { + ID string `json:"id,omitempty"` + Index string `json:"index,omitempty"` + Type string `json:"type,omitempty"` + Path string `json:"path,omitempty"` + Routing string `json:"routing,omitempty"` +} + +// NewGeoShapeQuery creates and initializes a new GeoShapeQuery. +func NewGeoShapeQuery(path string) *GeoShapeQuery { + return &GeoShapeQuery{ + path: path, + } +} + +// IndexedShape adds a IndexedShape that represents a shape which has already been indexed in +// another index and/or index type. +func (q *GeoShapeQuery) IndexedShape(id, index, typ, path, routing string) *GeoShapeQuery { + q.indexedShape = &IndexedShape{ + ID: id, + Index: index, + Type: typ, + Path: path, + Routing: routing, + } + return q +} + +// AddPoint adds a Point that represents a single geographic coordinate. +func (q *GeoShapeQuery) AddPoint(coord []float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithPoint(coord)) + return q +} + +// AddMultiPoint adds a MultiPoint that represents an array of unconnected +// but likely related points. +func (q *GeoShapeQuery) AddMultiPoint(coord [][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithMultiPoint(coord)) + return q +} + +// AddLineString adds a LineString that represents an arbitray line given +// two or more points. +func (q *GeoShapeQuery) AddLineString(coord [][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithLineString(coord)) + return q +} + +// AddMultiLineString adds a MultLineString that represents an array of separate +// linestrings. +func (q *GeoShapeQuery) AddMultiLineString(coord [][][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithMultiLineString(coord)) + return q +} + +// AddPolygon adds a Polygon that represents a closed polygon whose first and last +// point must match, thus requiring n + 1 vertices to create an n- sided polygon and +// a minimum of 4 vertices. +func (q *GeoShapeQuery) AddPolygon(coord [][][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithPolygon(coord)) + return q +} + +// AddMultiPolygon adds a MultiPolyogn that represents an array of separated polygons. +func (q *GeoShapeQuery) AddMultiPolygon(coord [][][][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithMultiPolygon(coord)) + return q +} + +// AddGeometryCollection adds a GeometryCollection that represents a geo shape similar +// to the multi* shapes except that multiple types can coexist (e.g.: a Point and a LineString). +func (q *GeoShapeQuery) AddGeometryCollection(geometries []interface{}) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithGeometryCollection(geometries)) + return q +} + +// AddEnvelope adds an Envelope that represents an bounding rectangle. +func (q *GeoShapeQuery) AddEnvelope(coord [][]float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithEnvelope(coord)) + return q +} + +// AddCircle adds a Circle that represents a circle with radius in meters. +func (q *GeoShapeQuery) AddCircle(radius string, coord []float64) *GeoShapeQuery { + q.shape = geo.NewShape(geo.WithCircle(radius, coord)) + return q +} + +// Relation sets which spatial relation operators may ber used at search time. +func (q *GeoShapeQuery) Relation(relation string) *GeoShapeQuery { + q.relation = relation + return q +} + +func (q *GeoShapeQuery) QueryName(queryName string) *GeoShapeQuery { + q.queryName = queryName + return q +} + +// Source returns JSON for the function score query. +func (q *GeoShapeQuery) Source() (interface{}, error) { + path := make(map[string]interface{}) + + if q.indexedShape != nil { + // "geo_shape": { + // "location": { + // "indexed_shape": { + // "index": "shapes", + // "type": "_doc", + // "id": "deu", + // "path": "location" + // } + // } + // } + path["indexed_shape"] = q.indexedShape + } else if q.shape != nil { + // supports: + // - point + // - multipoint + // - linestring + // - multilinestring + // - polygon + // - multipolygon + // - envelope + // - circle + // - geometrycollection + // e..g: + // "geo_shape" : { + // "location": { + // "shape": { + // "type": "envelope" + // "coordinates": [[13.0, 53.0], [14.0, 52.0]] + // }, + // "relation": "within" + // } + // } + path["shape"] = q.shape + if q.relation != "" { + path["relation"] = q.relation + } + if q.queryName != "" { + path["_name"] = q.queryName + } + } + params := make(map[string]interface{}) + params[q.path] = path + + source := make(map[string]interface{}) + source["geo_shape"] = params + + return source, nil +} diff --git a/search_queries_geo_shape_test.go b/search_queries_geo_shape_test.go new file mode 100644 index 000000000..2c1b8f9be --- /dev/null +++ b/search_queries_geo_shape_test.go @@ -0,0 +1,217 @@ +// Copyright 2012-present Oliver Eilhard. All rights reserved. +// Use of this source code is governed by a MIT-license. +// See http://olivere.mit-license.org/license.txt for details. + +package elastic + +import ( + "encoding/json" + "testing" + + "github.com/olivere/elastic/geo" + "github.com/stretchr/testify/require" +) + +func TestGeoShapeQueryIndexedShape(t *testing.T) { + var ( + id = "deu" + index = "shapes" + itype = "_doc" + path = "location" + routing = "" + expected = []byte(`{"geo_shape":{"location":{"indexed_shape":{"index":"shapes","type":"_doc","id":"deu","path":"location"}}}}`) + ) + assert := require.New(t) + + q := NewGeoShapeQuery("location").IndexedShape(id, index, itype, path, routing) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryPoint(t *testing.T) { + var ( + coordinates = []float64{13.400544, 52.530286} + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"point","coordinates":[13.400544,52.530286]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location") + q = q.AddPoint(coordinates) + q = q.Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryLineString(t *testing.T) { + var ( + coordinates = [][]float64{{-77.03653, 38.897676}, {-77.009051, 38.889939}} + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"linestring","coordinates":[[-77.03653, 38.897676],[-77.009051, 38.889939]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddLineString(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryPolygon(t *testing.T) { + var ( + coordinates = [][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + } + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"polygon","coordinates":[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddPolygon(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryMultiPoint(t *testing.T) { + var ( + coordinates = [][]float64{{102.0, 2.0}, {103.0, 2.0}} + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"multipoint","coordinates":[[102.0, 2.0],[103.0, 2.0]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddMultiPoint(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryMultiLineString(t *testing.T) { + var ( + coordinates = [][][]float64{ + [][]float64{ + {100.0, 0.0}, {101.0, 1.0}, + }, + [][]float64{ + {102.0, 2.0}, {103.0, 3.0}, + }, + } + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"multilinestring","coordinates":[[[100.0,0.0],[101.0,1.0]],[[102.0,2.0],[103.0,3.0]]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddMultiLineString(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryMultiPolygon(t *testing.T) { + var ( + coordinates = [][][][]float64{ + { + [][]float64{ + {102.0, 2.0}, {103.0, 2.0}, {103.0, 3.0}, {102.0, 3.0}, {102.0, 2.0}, + }, + }, + { + [][]float64{ + {100.0, 0.0}, {101.0, 0.0}, {101.0, 1.0}, {100.0, 1.0}, {100.0, 0.0}, + }, + [][]float64{ + {100.2, 0.2}, {100.8, 0.2}, {100.8, 0.8}, {100.2, 0.8}, {100.2, 0.2}, + }, + }, + } + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"multipolygon","coordinates":[[[[102.0,2.0],[103.0,2.0],[103.0,3.0],[102.0,3.0],[102.0,2.0]]],[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]],[[100.2,0.2],[100.8,0.2],[100.8,0.8],[100.2,0.8],[100.2,0.2]]]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddMultiPolygon(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryGeometryCollection(t *testing.T) { + var ( + geometries = []interface{}{ + geo.NewPoint([]float64{100.0, 0.0}), + geo.NewLineString([][]float64{{101.0, 0.0}, {102.0, 1.0}}), + } + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"geometrycollection","geometries":[{"type":"point","coordinates":[100.0,0.0]},{"type":"linestring","coordinates":[[101.0,0.0],[102.0,1.0]]}]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddGeometryCollection(geometries).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryEnvelope(t *testing.T) { + var ( + coordinates = [][]float64{{100.0, 1.0}, {101.0, 0.0}} + relation = "within" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"envelope","coordinates":[[100.0,1.0],[101.0,0.0]]},"relation":"within"}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddEnvelope(coordinates).Relation(relation) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} + +func TestGeoShapeQueryCircle(t *testing.T) { + var ( + coordinates = []float64{-109.874838, 44.439550} + radius = "25m" + expected = []byte(`{"geo_shape":{"location":{"shape":{"type":"circle","radius":"25m","coordinates":[-109.874838,44.439550]}}}}`) + ) + assert := require.New(t) + q := NewGeoShapeQuery("location").AddCircle(radius, coordinates) + + src, err := q.Source() + assert.NoError(err) + + actual, err := json.Marshal(src) + assert.NoError(err) + assert.JSONEq(string(expected), string(actual)) +} From 7e22a125109469d516239582d4e92749e2c6775d Mon Sep 17 00:00:00 2001 From: hmarques Date: Sat, 16 Mar 2019 16:19:25 -0300 Subject: [PATCH 2/2] Fix the travis and update README.md Change-Id: I72ce5d9647ce5c55924cb563db49de4253ae792c --- .travis.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6bc5beff8..7248bf240 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ before_install: - docker-compose up -d - go get -u github.com/google/go-cmp/cmp - go get -u github.com/fortytw2/leaktest +- go get -u github.com/stretchr/testify/require - go get . ./aws/... ./config/... ./trace/... ./uritemplates/... - while ! nc -z localhost 9200; do sleep 1; done - while ! nc -z localhost 9210; do sleep 1; done diff --git a/README.md b/README.md index 159a6bdc1..cab941440 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ The cat APIs are not implemented as of now. We think they are better suited for - [x] Has Parent Query - [x] Parent Id Query - Geo queries - - [ ] GeoShape Query + - [x] GeoShape Query - [x] Geo Bounding Box Query - [x] Geo Distance Query - [x] Geo Polygon Query