348 lines
9.2 KiB
Go
348 lines
9.2 KiB
Go
|
package geojson
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
)
|
||
|
|
||
|
// A GeometryType serves to enumerate the different GeoJSON geometry types.
|
||
|
type GeometryType string
|
||
|
|
||
|
// The geometry types supported by GeoJSON 1.0
|
||
|
const (
|
||
|
GeometryPoint GeometryType = "Point"
|
||
|
GeometryMultiPoint GeometryType = "MultiPoint"
|
||
|
GeometryLineString GeometryType = "LineString"
|
||
|
GeometryMultiLineString GeometryType = "MultiLineString"
|
||
|
GeometryPolygon GeometryType = "Polygon"
|
||
|
GeometryMultiPolygon GeometryType = "MultiPolygon"
|
||
|
GeometryCollection GeometryType = "GeometryCollection"
|
||
|
)
|
||
|
|
||
|
// A Geometry correlates to a GeoJSON geometry object.
|
||
|
type Geometry struct {
|
||
|
Type GeometryType `json:"type"`
|
||
|
BoundingBox []float64 `json:"bbox,omitempty"`
|
||
|
Point []float64
|
||
|
MultiPoint [][]float64
|
||
|
LineString [][]float64
|
||
|
MultiLineString [][][]float64
|
||
|
Polygon [][][]float64
|
||
|
MultiPolygon [][][][]float64
|
||
|
Geometries []*Geometry
|
||
|
CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported
|
||
|
}
|
||
|
|
||
|
// NewPointGeometry creates and initializes a point geometry with the give coordinate.
|
||
|
func NewPointGeometry(coordinate []float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryPoint,
|
||
|
Point: coordinate,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewMultiPointGeometry creates and initializes a multi-point geometry with the given coordinates.
|
||
|
func NewMultiPointGeometry(coordinates ...[]float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryMultiPoint,
|
||
|
MultiPoint: coordinates,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewLineStringGeometry creates and initializes a line string geometry with the given coordinates.
|
||
|
func NewLineStringGeometry(coordinates [][]float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryLineString,
|
||
|
LineString: coordinates,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewMultiLineStringGeometry creates and initializes a multi-line string geometry with the given lines.
|
||
|
func NewMultiLineStringGeometry(lines ...[][]float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryMultiLineString,
|
||
|
MultiLineString: lines,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewPolygonGeometry creates and initializes a polygon geometry with the given polygon.
|
||
|
func NewPolygonGeometry(polygon [][][]float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryPolygon,
|
||
|
Polygon: polygon,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewMultiPolygonGeometry creates and initializes a multi-polygon geometry with the given polygons.
|
||
|
func NewMultiPolygonGeometry(polygons ...[][][]float64) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryMultiPolygon,
|
||
|
MultiPolygon: polygons,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewCollectionGeometry creates and initializes a geometry collection geometry with the given geometries.
|
||
|
func NewCollectionGeometry(geometries ...*Geometry) *Geometry {
|
||
|
return &Geometry{
|
||
|
Type: GeometryCollection,
|
||
|
Geometries: geometries,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// MarshalJSON converts the geometry object into the correct JSON.
|
||
|
// This fulfills the json.Marshaler interface.
|
||
|
func (g Geometry) MarshalJSON() ([]byte, error) {
|
||
|
// defining a struct here lets us define the order of the JSON elements.
|
||
|
type geometry struct {
|
||
|
Type GeometryType `json:"type"`
|
||
|
BoundingBox []float64 `json:"bbox,omitempty"`
|
||
|
Coordinates interface{} `json:"coordinates,omitempty"`
|
||
|
Geometries interface{} `json:"geometries,omitempty"`
|
||
|
CRS map[string]interface{} `json:"crs,omitempty"`
|
||
|
}
|
||
|
|
||
|
geo := &geometry{
|
||
|
Type: g.Type,
|
||
|
}
|
||
|
|
||
|
if g.BoundingBox != nil && len(g.BoundingBox) != 0 {
|
||
|
geo.BoundingBox = g.BoundingBox
|
||
|
}
|
||
|
|
||
|
switch g.Type {
|
||
|
case GeometryPoint:
|
||
|
geo.Coordinates = g.Point
|
||
|
case GeometryMultiPoint:
|
||
|
geo.Coordinates = g.MultiPoint
|
||
|
case GeometryLineString:
|
||
|
geo.Coordinates = g.LineString
|
||
|
case GeometryMultiLineString:
|
||
|
geo.Coordinates = g.MultiLineString
|
||
|
case GeometryPolygon:
|
||
|
geo.Coordinates = g.Polygon
|
||
|
case GeometryMultiPolygon:
|
||
|
geo.Coordinates = g.MultiPolygon
|
||
|
case GeometryCollection:
|
||
|
geo.Geometries = g.Geometries
|
||
|
}
|
||
|
|
||
|
return json.Marshal(geo)
|
||
|
}
|
||
|
|
||
|
// UnmarshalGeometry decodes the data into a GeoJSON geometry.
|
||
|
// Alternately one can call json.Unmarshal(g) directly for the same result.
|
||
|
func UnmarshalGeometry(data []byte) (*Geometry, error) {
|
||
|
g := &Geometry{}
|
||
|
err := json.Unmarshal(data, g)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
// UnmarshalJSON decodes the data into a GeoJSON geometry.
|
||
|
// This fulfills the json.Unmarshaler interface.
|
||
|
func (g *Geometry) UnmarshalJSON(data []byte) error {
|
||
|
var object map[string]interface{}
|
||
|
err := json.Unmarshal(data, &object)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return decodeGeometry(g, object)
|
||
|
}
|
||
|
|
||
|
// Scan implements the sql.Scanner interface allowing
|
||
|
// geometry structs to be passed into rows.Scan(...interface{})
|
||
|
// The columns must be received as GeoJSON Geometry.
|
||
|
// When using PostGIS a spatial column would need to be wrapped in ST_AsGeoJSON.
|
||
|
func (g *Geometry) Scan(value interface{}) error {
|
||
|
var data []byte
|
||
|
|
||
|
switch value.(type) {
|
||
|
case string:
|
||
|
data = []byte(value.(string))
|
||
|
case []byte:
|
||
|
data = value.([]byte)
|
||
|
default:
|
||
|
return errors.New("unable to parse this type into geojson")
|
||
|
}
|
||
|
|
||
|
return g.UnmarshalJSON(data)
|
||
|
}
|
||
|
|
||
|
func decodeGeometry(g *Geometry, object map[string]interface{}) error {
|
||
|
t, ok := object["type"]
|
||
|
if !ok {
|
||
|
return errors.New("type property not defined")
|
||
|
}
|
||
|
|
||
|
if s, ok := t.(string); ok {
|
||
|
g.Type = GeometryType(s)
|
||
|
} else {
|
||
|
return errors.New("type property not string")
|
||
|
}
|
||
|
|
||
|
bb, err := decodeBoundingBox(object["bbox"])
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
g.BoundingBox = bb
|
||
|
|
||
|
switch g.Type {
|
||
|
case GeometryPoint:
|
||
|
g.Point, err = decodePosition(object["coordinates"])
|
||
|
case GeometryMultiPoint:
|
||
|
g.MultiPoint, err = decodePositionSet(object["coordinates"])
|
||
|
case GeometryLineString:
|
||
|
g.LineString, err = decodePositionSet(object["coordinates"])
|
||
|
case GeometryMultiLineString:
|
||
|
g.MultiLineString, err = decodePathSet(object["coordinates"])
|
||
|
case GeometryPolygon:
|
||
|
g.Polygon, err = decodePathSet(object["coordinates"])
|
||
|
case GeometryMultiPolygon:
|
||
|
g.MultiPolygon, err = decodePolygonSet(object["coordinates"])
|
||
|
case GeometryCollection:
|
||
|
g.Geometries, err = decodeGeometries(object["geometries"])
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func decodePosition(data interface{}) ([]float64, error) {
|
||
|
coords, ok := data.([]interface{})
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("not a valid position, got %v", data)
|
||
|
}
|
||
|
|
||
|
result := make([]float64, 0, len(coords))
|
||
|
for _, coord := range coords {
|
||
|
if f, ok := coord.(float64); ok {
|
||
|
result = append(result, f)
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("not a valid coordinate, got %v", coord)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func decodePositionSet(data interface{}) ([][]float64, error) {
|
||
|
points, ok := data.([]interface{})
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("not a valid set of positions, got %v", data)
|
||
|
}
|
||
|
|
||
|
result := make([][]float64, 0, len(points))
|
||
|
for _, point := range points {
|
||
|
if p, err := decodePosition(point); err == nil {
|
||
|
result = append(result, p)
|
||
|
} else {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func decodePathSet(data interface{}) ([][][]float64, error) {
|
||
|
sets, ok := data.([]interface{})
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("not a valid path, got %v", data)
|
||
|
}
|
||
|
|
||
|
result := make([][][]float64, 0, len(sets))
|
||
|
|
||
|
for _, set := range sets {
|
||
|
if s, err := decodePositionSet(set); err == nil {
|
||
|
result = append(result, s)
|
||
|
} else {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func decodePolygonSet(data interface{}) ([][][][]float64, error) {
|
||
|
polygons, ok := data.([]interface{})
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("not a valid polygon, got %v", data)
|
||
|
}
|
||
|
|
||
|
result := make([][][][]float64, 0, len(polygons))
|
||
|
for _, polygon := range polygons {
|
||
|
if p, err := decodePathSet(polygon); err == nil {
|
||
|
result = append(result, p)
|
||
|
} else {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func decodeGeometries(data interface{}) ([]*Geometry, error) {
|
||
|
if vs, ok := data.([]interface{}); ok {
|
||
|
geometries := make([]*Geometry, 0, len(vs))
|
||
|
for _, v := range vs {
|
||
|
g := &Geometry{}
|
||
|
|
||
|
vmap, ok := v.(map[string]interface{})
|
||
|
if !ok {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
err := decodeGeometry(g, vmap)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
geometries = append(geometries, g)
|
||
|
}
|
||
|
|
||
|
if len(geometries) == len(vs) {
|
||
|
return geometries, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, fmt.Errorf("not a valid set of geometries, got %v", data)
|
||
|
}
|
||
|
|
||
|
// IsPoint returns true with the geometry object is a Point type.
|
||
|
func (g *Geometry) IsPoint() bool {
|
||
|
return g.Type == GeometryPoint
|
||
|
}
|
||
|
|
||
|
// IsMultiPoint returns true with the geometry object is a MultiPoint type.
|
||
|
func (g *Geometry) IsMultiPoint() bool {
|
||
|
return g.Type == GeometryMultiPoint
|
||
|
}
|
||
|
|
||
|
// IsLineString returns true with the geometry object is a LineString type.
|
||
|
func (g *Geometry) IsLineString() bool {
|
||
|
return g.Type == GeometryLineString
|
||
|
}
|
||
|
|
||
|
// IsMultiLineString returns true with the geometry object is a LineString type.
|
||
|
func (g *Geometry) IsMultiLineString() bool {
|
||
|
return g.Type == GeometryMultiLineString
|
||
|
}
|
||
|
|
||
|
// IsPolygon returns true with the geometry object is a Polygon type.
|
||
|
func (g *Geometry) IsPolygon() bool {
|
||
|
return g.Type == GeometryPolygon
|
||
|
}
|
||
|
|
||
|
// IsMultiPolygon returns true with the geometry object is a MultiPolygon type.
|
||
|
func (g *Geometry) IsMultiPolygon() bool {
|
||
|
return g.Type == GeometryMultiPolygon
|
||
|
}
|
||
|
|
||
|
// IsCollection returns true with the geometry object is a GeometryCollection type.
|
||
|
func (g *Geometry) IsCollection() bool {
|
||
|
return g.Type == GeometryCollection
|
||
|
}
|