Skip to content
Snippets Groups Projects
location.go 3.38 KiB
package field

import (
	"context"

	"git.perx.ru/perxis/perxis-go/pkg/errors"
	"github.com/mitchellh/mapstructure"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/x/bsonx"
)

var locationType = &LocationType{}

type LocationParameters struct{}

func (p LocationParameters) Type() Type                  { return locationType }
func (p LocationParameters) Clone(reset bool) Parameters { return &LocationParameters{} }

func (p LocationParameters) GetMongoIndexes(path string, f *Field) []mongo.IndexModel {
	var add, geo mongo.IndexModel
	a := path + ".address"
	g := path + ".geometry"
	add.Options = options.Index().SetName(a)
	//add.Options.SetSparse(true)
	add.Options.SetPartialFilterExpression(bson.M{a: bson.M{"$exists": true}})
	geo.Options = options.Index().SetName(g)
	if f.Unique {
		add.Options.SetUnique(true)
		geo.Options.SetUnique(true)
	}

	if f.Indexed {
		add.Keys = bsonx.Doc{{Key: a, Value: bsonx.Int32(1)}}
		geo.Keys = bsonx.Doc{{Key: g, Value: bsonx.String("2dsphere")}}
	}
	return []mongo.IndexModel{add, geo}
}

type LocationType struct{}

type GeoJSON struct {
	Type        string    `json:"type" bson:"type" mapstructure:"type,omitempty"`
	Coordinates []float64 `json:"coordinates" bson:"coordinates" mapstructure:"coordinates"`
}

type GeoObject struct {
	Address  string   `json:"address,omitempty" bson:"address" mapstructure:"address,omitempty"`
	Geometry *GeoJSON `json:"geometry,omitempty" bson:"geometry" mapstructure:"geometry,omitempty"`
}

func (LocationType) Name() string {
	return "location"
}

func (LocationType) NewParameters() Parameters {
	return &LocationParameters{}
}

func (LocationType) IsEmpty(v interface{}) bool {
	loc, _ := v.(*GeoObject)
	return loc == nil || loc.Address != "" && loc.Geometry != nil
}

func (LocationType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {

	if v == nil {
		return nil, nil
	}

	var g GeoObject
	if err := mapstructure.Decode(v, &g); err != nil {
		return nil, err
	}

	if g.Address == "" && g.Geometry == nil {
		return nil, errors.New("address or coordinates required")
	}

	if g.Geometry != nil {
		if len(g.Geometry.Coordinates) != 2 {
			return nil, errors.New("latitude and longitude required")
		}

		lat := g.Geometry.Coordinates[0]
		lon := g.Geometry.Coordinates[1]

		if lat < -180 || lat > 180 {
			return nil, errors.New("invalid longitude values, valid are between -180 and 180")
		}

		if lon < -90 || lon > 90 {
			return nil, errors.New("invalid latitude values, valid are between -90 and 90")
		}

		if g.Geometry.Type != "Point" {
			g.Geometry.Type = "Point"
		}
	}

	return &g, nil
}

func (LocationType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {

	if v == nil {
		return nil, nil
	}

	g, ok := v.(*GeoObject)
	if !ok {
		return nil, errors.New("couldn't encode GeoObject")
	}

	res := make(map[string]interface{})
	if g.Address != "" {
		res["address"] = g.Address
	}

	if g.Geometry != nil {
		if len(g.Geometry.Coordinates) != 2 {
			return nil, errors.New("latitude and longitude required")
		}

		lat := g.Geometry.Coordinates[0]
		lon := g.Geometry.Coordinates[1]

		res["geometry"] = map[string]interface{}{"type": g.Geometry.Type, "coordinates": []interface{}{lat, lon}}
	}

	return res, nil
}

func Location(o ...interface{}) *Field {
	return NewField(&LocationParameters{}, o...)
}