spot

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 2 Imported by: 0

README

spot

Go Reference

RFC7946 GeoJSON geometry types for Go, with Elasticsearch geo-shape support.

This module models all nine geometry types as first-class Go structs with typed marshal/unmarshal: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, Envelope, and Circle.

All types implement the Geometry interface (GeoType() string), so a GeometryCollection carries []Geometry rather than []any.

Type name casing: this library uses lowercase type names (e.g. "point", "polygon") as required by Elasticsearch. This differs from RFC 7946, which specifies PascalCase ("Point", "Polygon").

This module has no runtime dependency on any Elasticsearch client.

Install

go get github.com/heltonmarx/spot

Usage

1. Indexing a document with a geo_shape field

When you index a document, Elasticsearch expects a GeoJSON geometry object for any geo_shape mapped field. spot gives you typed constructors instead of hand-assembled map[string]any:

doc := map[string]any{
    "name": "Central Park",
    "location": spot.NewPolygon([][][]float64{
        {
            {-73.98, 40.77}, {-73.95, 40.77},
            {-73.95, 40.76}, {-73.98, 40.76},
            {-73.98, 40.77},
        },
    }),
}

body, _ := json.Marshal(doc)
es.Index("places", strings.NewReader(string(body)))
2. Geo-shape queries

Find documents whose shape intersects, contains, or is within a query shape. Marshal a spot.Shape into GeoShapeFieldQuery.Shape from the official client:

import (
    "context"
    "encoding/json"

    elasticsearch "github.com/elastic/go-elasticsearch/v8"
    estypes "github.com/elastic/go-elasticsearch/v8/typedapi/types"
    "github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/geoshaperelation"
    "github.com/heltonmarx/spot"
)

func search(es *elasticsearch.TypedClient) {
    shapeBytes, _ := json.Marshal(spot.NewShape(spot.WithEnvelope([][]float64{
        {-74.1, 40.9}, {-73.9, 40.7},
    })))

    q := estypes.NewGeoShapeQuery()
    q.GeoShapeQuery["location"] = estypes.GeoShapeFieldQuery{
        Shape:    shapeBytes,
        Relation: &geoshaperelation.Intersects,
    }

    es.Search().Index("places").
        Request(&estypes.SearchRequest{
            Query: &estypes.Query{GeoShape: q},
        }).
        Do(context.Background())
}

Spatial relations map to real questions:

Relation Meaning
intersects shapes overlap at all (default)
within document shape is fully inside the query shape
contains document shape fully encloses the query shape
disjoint shapes have no overlap
3. Parsing shapes from Elasticsearch responses

When reading a document back from ES, the location field arrives as raw GeoJSON. Shape.UnmarshalJSON dispatches to the correct concrete type automatically (no manual type-switching required):

var shape spot.Shape
json.Unmarshal(hit["location"], &shape)

switch {
case shape.IsPolygon():
    rings := shape.Polygon.Coordinates
    // ...
case shape.IsPoint():
    lon, lat := shape.Point.Coordinates[0], shape.Point.Coordinates[1]
    // ...
}
4. GeometryCollection for mixed-type fields

Elasticsearch supports indexing a geometrycollection, for example a venue that has both a polygon boundary and a point entrance. GeometryCollection carries []Geometry, so you can build and inspect collections without type assertions:

collection := spot.NewGeometryCollection([]spot.Geometry{
    spot.NewPoint([]float64{-73.98, 40.76}),
    spot.NewPolygon([][][]float64{
        {
            {-73.99, 40.77}, {-73.97, 40.77},
            {-73.97, 40.75}, {-73.99, 40.75},
            {-73.99, 40.77},
        },
    }),
})
5. Envelope and Circle for bounding-box and radius queries

Envelope (top-left + bottom-right corners) and Circle (center + radius) are Elasticsearch extensions not in standard GeoJSON. They are the most efficient shapes for bounding-box and proximity queries:

// Bounding box: upper-left corner first, lower-right second.
envelope := spot.NewShape(spot.WithEnvelope([][]float64{
    {-74.1, 40.9},
    {-73.9, 40.7},
}))

// Circle: radius defaults to meters when no unit suffix is given.
circle := spot.NewShape(spot.WithCircle("5km", []float64{-73.98, 40.76}))

Supported geometry types

Type constant Shape helper GeoJSON object
TypePoint WithPoint point
TypeMultiPoint WithMultiPoint multipoint
TypeLineString WithLineString linestring
TypeMultiLineString WithMultiLineString multilinestring
TypePolygon WithPolygon polygon
TypeMultiPolygon WithMultiPolygon multipolygon
TypeGeometryCollection WithGeometryCollection geometrycollection
TypeEnvelope WithEnvelope envelope (ES extension)
TypeCircle WithCircle circle (ES extension)

Shape also exposes IsPoint(), IsPolygon(), ... predicates and a discriminated MarshalJSON/UnmarshalJSON, so a heterogeneous Shape can be round-tripped without knowing its concrete type.

Compatibility

spot uses lowercase type names ("point", "polygon") as required by Elasticsearch. Other GeoJSON consumers follow RFC 7946, which uses PascalCase ("Point", "Polygon"). Use the RFC7946 wrapper for those systems.

// Elasticsearch / OpenSearch (default, lowercase)
shapeBytes, _ := json.Marshal(shape)

// MongoDB, PostGIS, Solr, and any RFC 7946 consumer (PascalCase)
shapeBytes, _ := json.Marshal(spot.RFC7946(shape))

RFC7946 recurses into GeometryCollection members automatically. Envelope and Circle are Elasticsearch extensions with no RFC 7946 equivalent; their type names are left unchanged by the wrapper.

Consumer Type name format How to marshal
Elasticsearch lowercase json.Marshal(shape)
OpenSearch lowercase json.Marshal(shape)
CrateDB lowercase json.Marshal(shape)
MongoDB PascalCase (RFC 7946) json.Marshal(spot.RFC7946(shape))
PostgreSQL + PostGIS PascalCase (RFC 7946) json.Marshal(spot.RFC7946(shape))
Solr PascalCase (RFC 7946) json.Marshal(spot.RFC7946(shape))

Tests

go test ./...

License

Copyright (c) Helton Marques. Released under the MIT License.

Documentation

Overview

Package spot models GeoJSON geometry types (Point, LineString, Polygon, and their multi/collection variants) plus the additional shape types that the Elasticsearch geo-shape spatial strategy accepts: Envelope and Circle. Shapes unmarshal GeoJSON with type-aware dispatch and marshal back to the canonical form.

Type names are lowercase (e.g. "point", "polygon") as required by Elasticsearch. This differs from RFC 7946, which uses PascalCase, but Elasticsearch accepts only lowercase for geo-shape queries.

Example (BuildPointShape)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/heltonmarx/spot"
)

func main() {
	shape := spot.NewShape(spot.WithPoint([]float64{13.4, 52.5}))

	data, err := json.Marshal(shape)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}
Output:
{"type":"point","coordinates":[13.4,52.5]}
Example (BuildPolygonShape)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/heltonmarx/spot"
)

func main() {
	shape := spot.NewShape(spot.WithPolygon([][][]float64{
		{
			{13.4, 52.5},
			{14.4, 52.5},
			{14.4, 53.5},
			{13.4, 52.5},
		},
	}))

	data, err := json.Marshal(shape)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}
Output:
{"type":"polygon","coordinates":[[[13.4,52.5],[14.4,52.5],[14.4,53.5],[13.4,52.5]]]}
Example (GeoShapeQueryDSL)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/heltonmarx/spot"
)

func main() {
	shape := spot.NewShape(spot.WithPoint([]float64{13.4, 52.5}))

	shapeBytes, err := json.Marshal(shape)
	if err != nil {
		panic(err)
	}

	// shapeBytes is a json.RawMessage-compatible value, ready for
	// types.GeoShapeFieldQuery{Shape: shapeBytes} from go-elasticsearch/v8.
	fmt.Println(string(shapeBytes))
}
Output:
{"type":"point","coordinates":[13.4,52.5]}

Index

Examples

Constants

View Source
const (
	TypePoint              = "point"
	TypeMultiPoint         = "multipoint"
	TypeLineString         = "linestring"
	TypeMultiLineString    = "multilinestring"
	TypePolygon            = "polygon"
	TypeMultiPolygon       = "multipolygon"
	TypeGeometryCollection = "geometrycollection"
	TypeEnvelope           = "envelope"
	TypeCircle             = "circle"
)

The geometry types supported by Elasticsearch.

For more details, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#spatial-strategy

Variables

This section is empty.

Functions

This section is empty.

Types

type Circle

type Circle struct {
	Type        string    `json:"type"`
	Radius      string    `json:"radius"`
	Coordinates []float64 `json:"coordinates"`
}

Circle is specified by a center point and a radius with units, defaulting to meters when no unit suffix is present.

func NewCircle

func NewCircle(radius string, coordinates []float64) *Circle

NewCircle returns a Circle centered at the [lon, lat] coordinates with the given radius (e.g. "25m", "1km"). Panics if fewer than 2 coordinates are supplied or if radius is empty.

func (*Circle) GeoType

func (m *Circle) GeoType() string

GeoType returns the geometry type constant for Circle.

func (*Circle) UnmarshalJSON

func (m *Circle) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a circle geometry into m.

type Envelope

type Envelope struct {
	Type        string      `json:"type"`
	Coordinates [][]float64 `json:"coordinates"`
}

Envelope represents a bounding rectangle by the coordinates of its upper left and lower right corners, each a [lon, lat] position.

func NewEnvelope

func NewEnvelope(coordinates [][]float64) *Envelope

NewEnvelope returns an Envelope from its two corner coordinates. Panics if exactly two corner positions are not provided.

func (*Envelope) GeoType

func (m *Envelope) GeoType() string

GeoType returns the geometry type constant for Envelope.

func (*Envelope) UnmarshalJSON

func (m *Envelope) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes an envelope geometry into m.

type Geometry

type Geometry interface {
	GeoType() string
	// contains filtered or unexported methods
}

Geometry is implemented by all concrete geometry types in this package. The unexported isGeometry method seals the interface so that only types defined here can satisfy it.

type GeometryCollection

type GeometryCollection struct {
	Type       string     `json:"type"`
	Geometries []Geometry `json:"geometries"`
}

GeometryCollection is a collection of other geometry objects.

func NewGeometryCollection

func NewGeometryCollection(geometries []Geometry) *GeometryCollection

NewGeometryCollection returns a GeometryCollection of the given geometries.

func (*GeometryCollection) GeoType

func (m *GeometryCollection) GeoType() string

GeoType returns the geometry type constant for GeometryCollection.

func (*GeometryCollection) UnmarshalJSON

func (m *GeometryCollection) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON geometry collection into m, decoding each member geometry according to its own "type".

type LineString

type LineString struct {
	Type        string      `json:"type"`
	Coordinates [][]float64 `json:"coordinates"`
}

LineString is an array of two or more positions.

func NewLineString

func NewLineString(coordinates [][]float64) *LineString

NewLineString returns a LineString through the given [lon, lat] positions. Panics if fewer than 2 positions are supplied.

func (*LineString) GeoType

func (m *LineString) GeoType() string

GeoType returns the geometry type constant for LineString.

func (*LineString) UnmarshalJSON

func (m *LineString) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON linestring into m.

type MultiLineString

type MultiLineString struct {
	Type        string        `json:"type"`
	Coordinates [][][]float64 `json:"coordinates"`
}

MultiLineString is an array of LineString coordinate arrays.

func NewMultiLineString

func NewMultiLineString(coordinates [][][]float64) *MultiLineString

NewMultiLineString returns a MultiLineString, one [lon, lat] position array per line.

func (*MultiLineString) GeoType

func (m *MultiLineString) GeoType() string

GeoType returns the geometry type constant for MultiLineString.

func (*MultiLineString) UnmarshalJSON

func (m *MultiLineString) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON multilinestring into m.

type MultiPoint

type MultiPoint struct {
	Type        string      `json:"type"`
	Coordinates [][]float64 `json:"coordinates"`
}

MultiPoint is an array of positions.

func NewMultiPoint

func NewMultiPoint(coordinates [][]float64) *MultiPoint

NewMultiPoint returns a MultiPoint holding each [lon, lat] position.

func (*MultiPoint) GeoType

func (m *MultiPoint) GeoType() string

GeoType returns the geometry type constant for MultiPoint.

func (*MultiPoint) UnmarshalJSON

func (m *MultiPoint) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON multipoint into m.

type MultiPolygon

type MultiPolygon struct {
	Type        string          `json:"type"`
	Coordinates [][][][]float64 `json:"coordinates"`
}

MultiPolygon represents a GeoJSON object of multiple Polygons.

func NewMultiPolygon

func NewMultiPolygon(coordinates [][][][]float64) *MultiPolygon

NewMultiPolygon returns a MultiPolygon, one Polygon per top-level element.

func (*MultiPolygon) GeoType

func (m *MultiPolygon) GeoType() string

GeoType returns the geometry type constant for MultiPolygon.

func (*MultiPolygon) UnmarshalJSON

func (m *MultiPolygon) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON multipolygon into m.

type Option

type Option func(*Shape)

Option configures the geometry delivered by NewShape. Options are applied in order; later options overwrite the Type and geometry field set by earlier ones.

func WithCircle

func WithCircle(radius string, coordinates []float64) Option

WithCircle sets the Shape to a Circle with the given radius and center coordinates. radius defaults to meters unless it carries a unit suffix.

func WithEnvelope

func WithEnvelope(coordinates [][]float64) Option

WithEnvelope sets the Shape to an Envelope from its two corner coordinates.

func WithGeometryCollection

func WithGeometryCollection(geometries []Geometry) Option

WithGeometryCollection sets the Shape to a GeometryCollection of the given decoded geometries.

func WithLineString

func WithLineString(coordinates [][]float64) Option

WithLineString sets the Shape to a LineString made of [lon, lat] positions.

func WithMultiLineString

func WithMultiLineString(coordinates [][][]float64) Option

WithMultiLineString sets the Shape to a MultiLineString, one [lon, lat] position array per line.

func WithMultiPoint

func WithMultiPoint(coordinates [][]float64) Option

WithMultiPoint sets the Shape to a MultiPoint holding each [lon, lat] position.

func WithMultiPolygon

func WithMultiPolygon(coordinates [][][][]float64) Option

WithMultiPolygon sets the Shape to a MultiPolygon, one polygon per top-level element.

func WithPoint

func WithPoint(coordinates []float64) Option

WithPoint sets the Shape to a Point with the given [lon, lat] coordinates.

func WithPolygon

func WithPolygon(coordinates [][][]float64) Option

WithPolygon sets the Shape to a Polygon. The outer slice is one ring per linear ring (first the exterior, then each hole); every ring is a closed sequence of [lon, lat] positions.

type Point

type Point struct {
	Type        string    `json:"type"`
	Coordinates []float64 `json:"coordinates"`
}

Point is a single GeoJSON position.

func NewPoint

func NewPoint(coordinates []float64) *Point

NewPoint returns a Point positioned at the given [lon, lat] coordinates. Panics if fewer than 2 coordinates are supplied.

func (*Point) GeoType

func (m *Point) GeoType() string

GeoType returns the geometry type constant for Point.

func (*Point) UnmarshalJSON

func (m *Point) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON point into m.

type Polygon

type Polygon struct {
	Type        string        `json:"type"`
	Coordinates [][][]float64 `json:"coordinates"`
}

Polygon is an object consisting of one or more linear rings: the first ring is the exterior boundary and each subsequent ring is a hole. Rings are closed [lon, lat] position sequences (first and last position equal).

func NewPolygon

func NewPolygon(coordinates [][][]float64) *Polygon

NewPolygon returns a Polygon from its array of linear rings.

func (*Polygon) GeoType

func (m *Polygon) GeoType() string

GeoType returns the geometry type constant for Polygon.

func (*Polygon) UnmarshalJSON

func (m *Polygon) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON polygon into m.

type RFC7946Geometry

type RFC7946Geometry struct {
	// contains filtered or unexported fields
}

RFC7946Geometry wraps a Geometry and marshals it with PascalCase type names as required by RFC 7946. Use it when targeting GeoJSON consumers that follow the standard: MongoDB, PostGIS, Solr, and any RFC 7946-strict API.

Envelope and Circle are Elasticsearch extensions with no RFC 7946 equivalent; their type names are left unchanged.

func RFC7946

func RFC7946(g Geometry) RFC7946Geometry

RFC7946 wraps g so that MarshalJSON emits RFC 7946-compliant PascalCase type names instead of the Elasticsearch-compatible lowercase names.

func (RFC7946Geometry) MarshalJSON

func (r RFC7946Geometry) MarshalJSON() ([]byte, error)

MarshalJSON marshals the wrapped geometry with PascalCase type names. GeometryCollection members are recursed through the RFC7946 wrapper via a type assertion, avoiding JSON field-name inspection.

type Shape

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:"-"`
}

Shape is a discriminated container that can hold any single geometry: its Type field names the geometry kind, and exactly one concrete geometry field (Point, LineString, Polygon, etc.) is set to carry the value. Use it when the geometry type is not known at compile time — e.g. when unmarshaling arbitrary GeoJSON.

func NewShape

func NewShape(opts ...Option) *Shape

NewShape builds an empty generic Shape, then applies each option in order to set its Type and the corresponding geometry field. Common choices are the WithPoint, WithLineString, WithPolygon, ... helpers. NewShape returns a Shape whose Type is empty until at least one option is supplied.

func (*Shape) IsCircle

func (m *Shape) IsCircle() bool

IsCircle reports whether m holds a valid Circle, i.e. its type is TypeCircle and its Circle field is set.

func (*Shape) IsEnvelope

func (m *Shape) IsEnvelope() bool

IsEnvelope reports whether m holds a valid Envelope, i.e. its type is TypeEnvelope and its Envelope field is set.

func (*Shape) IsGeometryCollection

func (m *Shape) IsGeometryCollection() bool

IsGeometryCollection reports whether m holds a valid GeometryCollection, i.e. its type is TypeGeometryCollection and its GeometryCollection field is set.

func (*Shape) IsLineString

func (m *Shape) IsLineString() bool

IsLineString reports whether m holds a valid LineString, i.e. its type is TypeLineString and its LineString field is set.

func (*Shape) IsMultiLineString

func (m *Shape) IsMultiLineString() bool

IsMultiLineString reports whether m holds a valid MultiLineString, i.e. its type is TypeMultiLineString and its MultiLineString field is set.

func (*Shape) IsMultiPoint

func (m *Shape) IsMultiPoint() bool

IsMultiPoint reports whether m holds a valid MultiPoint, i.e. its type is TypeMultiPoint and its MultiPoint field is set.

func (*Shape) IsMultiPolygon

func (m *Shape) IsMultiPolygon() bool

IsMultiPolygon reports whether m holds a valid MultiPolygon, i.e. its type is TypeMultiPolygon and its MultiPolygon field is set.

func (*Shape) IsPoint

func (m *Shape) IsPoint() bool

IsPoint reports whether m holds a valid Point, i.e. its type is TypePoint and its Point field is set.

func (*Shape) IsPolygon

func (m *Shape) IsPolygon() bool

IsPolygon reports whether m holds a valid Polygon, i.e. its type is TypePolygon and its Polygon field is set.

func (*Shape) MarshalJSON

func (m *Shape) MarshalJSON() ([]byte, error)

MarshalJSON serializes the contained geometry as a GeoJSON object, emitting the raw fields of whichever concrete geometry is set. Returns an error if no geometry type is matched by m.

func (*Shape) UnmarshalJSON

func (m *Shape) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GeoJSON geometry into m. It reads the "type" field and decodes the remaining fields into the matching concrete geometry, storing the result in the corresponding Shape field. Returns an error if the "type" is not a recognized geometry kind.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL