From 3e8aea472934c4973635c08667de0d9c33d74fee Mon Sep 17 00:00:00 2001 From: Paul Mach Date: Wed, 3 Sep 2014 12:30:48 -0700 Subject: [PATCH] v0.1 --- .travis.yml | 13 ++ LICENSE | 18 +++ README.md | 100 ++++++++++++ feature.go | 83 ++++++++++ feature_collection.go | 55 +++++++ feature_collection_test.go | 77 +++++++++ feature_test.go | 48 ++++++ geometry.go | 323 +++++++++++++++++++++++++++++++++++++ geometry_test.go | 264 ++++++++++++++++++++++++++++++ properties.go | 122 ++++++++++++++ properties_test.go | 174 ++++++++++++++++++++ 11 files changed, 1277 insertions(+) create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 feature.go create mode 100644 feature_collection.go create mode 100644 feature_collection_test.go create mode 100644 feature_test.go create mode 100644 geometry.go create mode 100644 geometry_test.go create mode 100644 properties.go create mode 100644 properties_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aa459df --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.2 + - 1.3 + +after_script: + - FIXED=$(go vet ./... | wc -l); if [ $FIXED -gt 0 ]; then echo "go vet - $FIXED issues(s), please fix." && exit 2; fi + - FIXED=$(go fmt ./... | wc -l); if [ $FIXED -gt 0 ]; then echo "gofmt - $FIXED file(s) not formatted correctly, please run gofmt to fix this." && exit 2; fi + +script: + - go test -v + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..47c6e25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e41cc7b --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +go.geojson +========== + +go.geojson is a library for **encoding and decoding** [GeoJSON](http://geojson.org/) into Go structs. +Supports both the [json.Marshaler](http://golang.org/pkg/encoding/json/#Marshaler) and [json.Unmarshaler](http://golang.org/pkg/encoding/json/#Unmarshaler) +interfaces as well as helper functions such as `UnmarshalFeatureCollection`, `UnmarshalFeature` and `UnmarshalGeometry`. + +#### To install + + go get github.com/paulmach/go.geojson + +#### To use, imports as package name `geojson`: + + import "github.com/paulmach/go.geojson" + +
+[![Build Status](https://travis-ci.org/paulmach/go.geojson.png?branch=master)](https://travis-ci.org/paulmach/go.geojson) +    +[![Coverage Status](https://coveralls.io/repos/paulmach/go.geojson/badge.png?branch=master)](https://coveralls.io/r/paulmach/go.geojson?branch=master) +    +[![Godoc Reference](https://godoc.org/github.com/paulmach/go.geojson?status.png)](https://godoc.org/github.com/paulmach/go.geojson) + +## Examples + +* #### Unmarshalling (JSON -> Go) + + go.geojson supports both the [json.Marshaler](http://golang.org/pkg/encoding/json/#Marshaler) and [json.Unmarshaler](http://golang.org/pkg/encoding/json/#Unmarshaler) interfaces as well as helper functions such as `UnmarshalFeatureCollection`, `UnmarshalFeature` and `UnmarshalGeometry`. + + // Feature Collection + rawFeatureJSON := []byte(` + { "type": "FeatureCollection", + "features": [ + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": {"prop0": "value0"} + } + ] + }`) + + fc1, err := geojson.UnmarshalFeatureCollection(rawFeatureJSON) + + fc2 := geojson.NewFeatureCollection() + err := json.Unmarshal(rawJSON, fc2) + + // Geometry + rawGeometryJSON := []byte(`{"type": "Point", "coordinates": [102.0, 0.5]}`) + g, err := geojson.UnmarshalGeometry(rawGeometryJSON) + + g.IsPoint() == true + g.Point == []float64{102.0, 0.5} + + +* #### Marshalling (Go -> JSON) + + g := geojson.NewPointGeometry([]float64{1, 2}) + rawJSON, err := g.MarshalJSON() + + fc := geojson.NewFeatureCollection() + fc.AddFeature(geojson.NewPointFeature([]float64{1,2})) + rawJSON, err := fc.MarshalJSON() + +* #### Dealing with different Geometry types + + A geometry can be of several types, causing problems in a statically typed language. + Thus there is a separate attribute on Geometry for each type. + See the [Geometry object](https://godoc.org/github.com/paulmach/go.geojson#Geometry) for more details. + + g := UnmarshalGeometry([]byte(` + { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] + ] + }`)) + + switch { + case g.IsPoint(): + // do something with g.Point + case g.IsLineString(): + // do something with g.LineString + } + +## Feature Properties + +GeoJSON [Features](http://geojson.org/geojson-spec.html#feature-objects) can have properties of any type +causing issues in a statically typed language such as Go. +So, included are some helper methods on the Feature object to make the ease the pain. + + // functions to do the casting for you + func (f Feature) PropertyBool(key string) (bool, error) { + func (f Feature) PropertyInt(key string) (int, error) { + func (f Feature) PropertyFloat64(key string) (float64, error) { + func (f Feature) PropertyString(key string) (string, error) { + + // functions that hide the error and let you define default + func (f Feature) PropertyMustBool(key string, def ...bool) bool { + func (f Feature) PropertyMustInt(key string, def ...int) int { + func (f Feature) PropertyMustFloat64(key string, def ...float64) float64 { + func (f Feature) PropertyMustString(key string, def ...string) string { + diff --git a/feature.go b/feature.go new file mode 100644 index 0000000..3709fd8 --- /dev/null +++ b/feature.go @@ -0,0 +1,83 @@ +package geojson + +import ( + "encoding/json" +) + +// A Feature corresponds to GeoJSON feature object +type Feature struct { + ID string `json:"id,omitempty"` + Type string `json:"type"` + BoundingBox []float64 `json:"bbox,omitempty"` + Geometry *Geometry `json:"geometry"` + Properties map[string]interface{} `json:"properties"` + CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported +} + +// NewFeature creates and initializes a GeoJSON feature given the required attributes. +func NewFeature(geometry *Geometry) *Feature { + return &Feature{ + Type: "Feature", + Geometry: geometry, + Properties: make(map[string]interface{}), + } +} + +// NewPointFeature creates and initializes a GeoJSON feature with a point geometry using the given coordinate. +func NewPointFeature(coordinate []float64) *Feature { + return NewFeature(NewPointGeometry(coordinate)) +} + +// NewMultiPointFeature creates and initializes a GeoJSON feature with a multi-point geometry using the given coordinates. +func NewMultiPointFeature(coordinates ...[]float64) *Feature { + return NewFeature(NewMultiPointGeometry(coordinates...)) +} + +// NewLineStringFeature creates and initializes a GeoJSON feature with a line string geometry using the given coordinates. +func NewLineStringFeature(coordinates [][]float64) *Feature { + return NewFeature(NewLineStringGeometry(coordinates)) +} + +// NewMultiLineStringFeature creates and initializes a GeoJSON feature with a multi-line string geometry using the given lines. +func NewMultiLineStringFeature(lines ...[][]float64) *Feature { + return NewFeature(NewMultiLineStringGeometry(lines...)) +} + +// NewPolygonFeature creates and initializes a GeoJSON feature with a polygon geometry using the given polygon. +func NewPolygonFeature(polygon [][][]float64) *Feature { + return NewFeature(NewPolygonGeometry(polygon)) +} + +// NewMultiPolygonFeature creates and initializes a GeoJSON feature with a multi-polygon geometry using the given polygons. +func NewMultiPolygonFeature(polygons ...[][][]float64) *Feature { + return NewFeature(NewMultiPolygonGeometry(polygons...)) +} + +// NewCollectionFeature creates and initializes a GeoJSON feature with a geometry collection geometry using the given geometries. +func NewCollectionFeature(geometries ...*Geometry) *Feature { + return NewFeature(NewCollectionGeometry(geometries...)) +} + +// MarshalJSON converts the feature object into the proper JSON. +// It will handle the encoding of all the child geometries. +// Alternately one can call json.Marshal(f) directly for the same result. +func (f *Feature) MarshalJSON() ([]byte, error) { + f.Type = "Feature" + if len(f.Properties) == 0 { + f.Properties = nil + } + + return json.Marshal(*f) +} + +// UnmarshalFeature decodes the data into a GeoJSON feature. +// Alternately one can call json.Unmarshal(f) directly for the same result. +func UnmarshalFeature(data []byte) (*Feature, error) { + f := &Feature{} + err := json.Unmarshal(data, f) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/feature_collection.go b/feature_collection.go new file mode 100644 index 0000000..0da1236 --- /dev/null +++ b/feature_collection.go @@ -0,0 +1,55 @@ +/* +Package go.geojson is a library for encoding and decoding GeoJSON into Go structs. +Supports both the json.Marshaler and json.Unmarshaler interfaces as well as helper functions +such as `UnmarshalFeatureCollection`, `UnmarshalFeature` and `UnmarshalGeometry`. +*/ +package geojson + +import ( + "encoding/json" +) + +// A FeatureCollection correlates to a GeoJSON feature collection. +type FeatureCollection struct { + Type string `json:"type"` + BoundingBox []float64 `json:"bbox,omitempty"` + Features []*Feature `json:"features"` + CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported +} + +// NewFeatureCollection creates and initializes a new feature collection. +func NewFeatureCollection() *FeatureCollection { + return &FeatureCollection{ + Type: "FeatureCollection", + Features: make([]*Feature, 0), + } +} + +// AddFeature appends a feature to the collection. +func (fc *FeatureCollection) AddFeature(feature *Feature) *FeatureCollection { + fc.Features = append(fc.Features, feature) + return fc +} + +// MarshalJSON converts the feature collection object into the proper JSON. +// It will handle the encoding of all the child features and geometries. +// Alternately one can call json.Marshal(fc) directly for the same result. +func (fc *FeatureCollection) MarshalJSON() ([]byte, error) { + fc.Type = "FeatureCollection" + if fc.Features == nil { + fc.Features = make([]*Feature, 0) // GeoJSON requires the feature attribute to be at least [] + } + return json.Marshal(*fc) +} + +// UnmarshalFeatureCollection decodes the data into a GeoJSON feature collection. +// Alternately one can call json.Unmarshal(fc) directly for the same result. +func UnmarshalFeatureCollection(data []byte) (*FeatureCollection, error) { + fc := &FeatureCollection{} + err := json.Unmarshal(data, fc) + if err != nil { + return nil, err + } + + return fc, nil +} diff --git a/feature_collection_test.go b/feature_collection_test.go new file mode 100644 index 0000000..eeb471d --- /dev/null +++ b/feature_collection_test.go @@ -0,0 +1,77 @@ +package geojson + +import ( + "bytes" + "testing" +) + +func TestNewFeatureCollection(t *testing.T) { + fc := NewFeatureCollection() + + if fc.Type != "FeatureCollection" { + t.Errorf("should have type of FeatureCollection, got %v", fc.Type) + } +} + +func TestUnmarshalFeatureCollection(t *testing.T) { + rawJSON := ` + { "type": "FeatureCollection", + "features": [ + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": {"prop0": "value0"} + }, + { "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] + ] + }, + "properties": { + "prop0": "value0", + "prop1": 0.0 + } + }, + { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + }, + "properties": { + "prop0": "value0", + "prop1": {"this": "that"} + } + } + ] + }` + + fc, err := UnmarshalFeatureCollection([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal feature collection without issue, err %v", err) + } + + if fc.Type != "FeatureCollection" { + t.Errorf("should have type of FeatureCollection, got %v", fc.Type) + } + + if len(fc.Features) != 3 { + t.Errorf("should have 3 features but got %d", len(fc.Features)) + } +} + +func TestFeatureCollectionMarshalJSON(t *testing.T) { + fc := NewFeatureCollection() + blob, err := fc.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"features":[]`)) { + t.Errorf("json should set features object to at least empty array") + } +} diff --git a/feature_test.go b/feature_test.go new file mode 100644 index 0000000..2c0e71d --- /dev/null +++ b/feature_test.go @@ -0,0 +1,48 @@ +package geojson + +import ( + "bytes" + "testing" +) + +func TestNewFeature(t *testing.T) { + f := NewFeature(NewPointGeometry([]float64{1, 2})) + + if f.Type != "Feature" { + t.Errorf("should have type of Feature, got %v", f.Type) + } +} + +func TestFeatureMarshalJSON(t *testing.T) { + f := NewFeature(NewPointGeometry([]float64{1, 2})) + blob, err := f.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"properties":null`)) { + t.Errorf("json should set properties to null if there are none") + } +} + +func TestUnmarshalFeature(t *testing.T) { + rawJSON := ` + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": {"prop0": "value0"} + }` + + f, err := UnmarshalFeature([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal feature without issue, err %v", err) + } + + if f.Type != "Feature" { + t.Errorf("should have type of Feature, got %v", f.Type) + } + + if len(f.Properties) != 1 { + t.Errorf("should have 1 property but got %d", len(f.Properties)) + } +} diff --git a/geometry.go b/geometry.go new file mode 100644 index 0000000..643ea9e --- /dev/null +++ b/geometry.go @@ -0,0 +1,323 @@ +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) +} + +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") + } + + var err error + 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 +} diff --git a/geometry_test.go b/geometry_test.go new file mode 100644 index 0000000..fa728a5 --- /dev/null +++ b/geometry_test.go @@ -0,0 +1,264 @@ +package geojson + +import ( + "bytes" + "testing" +) + +func TestGeometryMarshalJSONPoint(t *testing.T) { + g := NewPointGeometry([]float64{1, 2}) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"Point"`)) { + t.Errorf("json should have type Point") + } + + if !bytes.Contains(blob, []byte(`[1,2]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONMultiPoint(t *testing.T) { + g := NewMultiPointGeometry([]float64{1, 2}, []float64{3, 4}) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"MultiPoint"`)) { + t.Errorf("json should have type MultiPoint") + } + + if !bytes.Contains(blob, []byte(`[[1,2],[3,4]]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONLineString(t *testing.T) { + g := NewLineStringGeometry([][]float64{[]float64{1, 2}, []float64{3, 4}}) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"LineString"`)) { + t.Errorf("json should have type LineString") + } + + if !bytes.Contains(blob, []byte(`[[1,2],[3,4]]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONMultiLineString(t *testing.T) { + g := NewMultiLineStringGeometry( + [][]float64{[]float64{1, 2}, []float64{3, 4}}, + [][]float64{[]float64{5, 6}, []float64{7, 8}}, + ) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"MultiLineString"`)) { + t.Errorf("json should have type MultiLineString") + } + + if !bytes.Contains(blob, []byte(`[[[1,2],[3,4]],[[5,6],[7,8]]]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONPolygon(t *testing.T) { + g := NewPolygonGeometry([][][]float64{ + [][]float64{[]float64{1, 2}, []float64{3, 4}}, + [][]float64{[]float64{5, 6}, []float64{7, 8}}, + }) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"Polygon"`)) { + t.Errorf("json should have type Polygon") + } + + if !bytes.Contains(blob, []byte(`[[[1,2],[3,4]],[[5,6],[7,8]]]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONMultiPolygon(t *testing.T) { + g := NewMultiPolygonGeometry( + [][][]float64{ + [][]float64{[]float64{1, 2}, []float64{3, 4}}, + [][]float64{[]float64{5, 6}, []float64{7, 8}}, + }, + [][][]float64{ + [][]float64{[]float64{8, 7}, []float64{6, 5}}, + [][]float64{[]float64{4, 3}, []float64{2, 1}}, + }) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"MultiPolygon"`)) { + t.Errorf("json should have type MultiPolygon") + } + + if !bytes.Contains(blob, []byte(`[[[[1,2],[3,4]],[[5,6],[7,8]]],[[[8,7],[6,5]],[[4,3],[2,1]]]]`)) { + t.Errorf("json should marshal coordinates correctly") + } +} + +func TestGeometryMarshalJSONCollection(t *testing.T) { + g := NewCollectionGeometry( + NewPointGeometry([]float64{1, 2}), + NewMultiPointGeometry([]float64{1, 2}, []float64{3, 4}), + ) + blob, err := g.MarshalJSON() + + if err != nil { + t.Fatalf("should marshal to json just fine but got %v", err) + } + + if !bytes.Contains(blob, []byte(`"type":"GeometryCollection"`)) { + t.Errorf("json should have type GeometryCollection") + } + + if !bytes.Contains(blob, []byte(`"geometries":`)) { + t.Errorf("json should have geometries attribute") + } +} + +func TestUnmarshalGeometryPoint(t *testing.T) { + rawJSON := `{"type": "Point", "coordinates": [102.0, 0.5]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "Point" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.Point) != 2 { + t.Errorf("should have 2 coordinate elements but got %d", len(g.Point)) + } +} + +func TestUnmarshalGeometryMultiPoint(t *testing.T) { + rawJSON := `{"type": "MultiPoint", "coordinates": [[1,2],[3,4]]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "MultiPoint" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.MultiPoint) != 2 { + t.Errorf("should have 2 coordinate elements but got %d", len(g.MultiPoint)) + } +} + +func TestUnmarshalGeometryLineString(t *testing.T) { + rawJSON := `{"type": "LineString", "coordinates": [[1,2],[3,4]]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "LineString" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.LineString) != 2 { + t.Errorf("should have 2 line string coordinates but got %d", len(g.LineString)) + } +} + +func TestUnmarshalGeometryMultiLineString(t *testing.T) { + rawJSON := `{"type": "MultiLineString", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "MultiLineString" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.MultiLineString) != 2 { + t.Errorf("should have 2 line strings but got %d", len(g.MultiLineString)) + } +} + +func TestUnmarshalGeometryPolygon(t *testing.T) { + rawJSON := `{"type": "Polygon", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "Polygon" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.Polygon) != 2 { + t.Errorf("should have 2 polygon paths but got %d", len(g.Polygon)) + } +} + +func TestUnmarshalGeometryMultiPolygon(t *testing.T) { + rawJSON := `{"type": "MultiPolygon", "coordinates": [[[[1,2],[3,4]],[[5,6],[7,8]]],[[[8,7],[6,5]],[[4,3],[2,1]]]]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "MultiPolygon" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.MultiPolygon) != 2 { + t.Errorf("should have 2 polygons but got %d", len(g.MultiPolygon)) + } +} + +func TestUnmarshalGeometryCollection(t *testing.T) { + rawJSON := `{"type": "GeometryCollection", "geometries": [ + {"type": "Point", "coordinates": [102.0, 0.5]}, + {"type": "MultiLineString", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]} + ]}` + + g, err := UnmarshalGeometry([]byte(rawJSON)) + if err != nil { + t.Fatalf("should unmarshal geometry without issue, err %v", err) + } + + if g.Type != "GeometryCollection" { + t.Errorf("incorrect type, got %v", g.Type) + } + + if len(g.Geometries) != 2 { + t.Errorf("should have 2 geometries but got %d", len(g.Geometries)) + } +} diff --git a/properties.go b/properties.go new file mode 100644 index 0000000..0ea327f --- /dev/null +++ b/properties.go @@ -0,0 +1,122 @@ +package geojson + +import ( + "fmt" +) + +// SetProperty provides the inverse of all the property functions +// and is here for consistency. +func (f *Feature) SetProperty(key string, value interface{}) { + if f.Properties == nil { + f.Properties = make(map[string]interface{}) + } + f.Properties[key] = value +} + +// PropertyBool type asserts a property to `bool`. +func (f Feature) PropertyBool(key string) (bool, error) { + if b, ok := (f.Properties[key]).(bool); ok { + return b, nil + } + return false, fmt.Errorf("type assertion of `%s` to bool failed", key) +} + +// PropertyInt type asserts a property to `int`. +func (f Feature) PropertyInt(key string) (int, error) { + if i, ok := (f.Properties[key]).(float64); ok { + return int(i), nil + } + return 0, fmt.Errorf("type assertion of `%s` to int failed", key) +} + +// PropertyFloat64 type asserts a property to `float64`. +func (f Feature) PropertyFloat64(key string) (float64, error) { + if i, ok := (f.Properties[key]).(float64); ok { + return i, nil + } + return 0, fmt.Errorf("type assertion of `%s` to float64 failed", key) +} + +// PropertyString type asserts a property to `string`. +func (f Feature) PropertyString(key string) (string, error) { + if s, ok := (f.Properties[key]).(string); ok { + return s, nil + } + return "", fmt.Errorf("type assertion of `%s` to string failed", key) +} + +// PropertyMustBool guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(f.PropertyMustBool("param1"), f.PropertyMustBool("optional_param", true)) +func (f Feature) PropertyMustBool(key string, def ...bool) bool { + var defaul bool + + b, err := f.PropertyBool(key) + if err == nil { + return b + } + + if len(def) > 0 { + defaul = def[0] + } + + return defaul +} + +// PropertyMustInt guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(f.PropertyMustInt("param1"), f.PropertyMustInt("optional_param", 123)) +func (f Feature) PropertyMustInt(key string, def ...int) int { + var defaul int + + b, err := f.PropertyInt(key) + if err == nil { + return b + } + + if len(def) > 0 { + defaul = def[0] + } + + return defaul +} + +// PropertyMustFloat64 guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(f.PropertyMustFloat64("param1"), f.PropertyMustFloat64("optional_param", 10.1)) +func (f Feature) PropertyMustFloat64(key string, def ...float64) float64 { + var defaul float64 + + b, err := f.PropertyFloat64(key) + if err == nil { + return b + } + + if len(def) > 0 { + defaul = def[0] + } + + return defaul +} + +// PropertyMustString guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(f.PropertyMustString("param1"), f.PropertyMustString("optional_param", "default")) +func (f Feature) PropertyMustString(key string, def ...string) string { + var defaul string + + b, err := f.PropertyString(key) + if err == nil { + return b + } + + if len(def) > 0 { + defaul = def[0] + } + + return defaul +} diff --git a/properties_test.go b/properties_test.go new file mode 100644 index 0000000..e3625d0 --- /dev/null +++ b/properties_test.go @@ -0,0 +1,174 @@ +package geojson + +import ( + "testing" +) + +func propertiesTestFeature() *Feature { + rawJSON := ` + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": {"bool":true,"falsebool":false,"int": 1,"float64": 1.2,"string":"text"} + }` + + f, _ := UnmarshalFeature([]byte(rawJSON)) + return f +} + +func TestFeatureSetProperty(t *testing.T) { + f := NewPointFeature([]float64{1, 2}) + f.Properties = nil + f.SetProperty("key", "value") + + if f.PropertyMustString("key") != "value" { + t.Errorf("property not set correctly") + } +} + +func TestFeaturePropertyBool(t *testing.T) { + f := propertiesTestFeature() + + _, err := f.PropertyBool("random") + if err == nil { + t.Errorf("should return error if invalid key") + } + + b, err := f.PropertyBool("bool") + if err != nil { + t.Errorf("should not return error if valid key") + } + + if b != true { + t.Errorf("should return proper property") + } +} + +func TestFeaturePropertyInt(t *testing.T) { + f := propertiesTestFeature() + + _, err := f.PropertyInt("random") + if err == nil { + t.Errorf("should return error if invalid key") + } + + i, err := f.PropertyInt("int") + if err != nil { + t.Errorf("should not return error if valid key") + } + + if i != 1 { + t.Errorf("should return proper property") + } +} + +func TestFeaturePropertyFloat64(t *testing.T) { + f := propertiesTestFeature() + + _, err := f.PropertyFloat64("random") + if err == nil { + t.Errorf("should return error if invalid key") + } + + i, err := f.PropertyFloat64("float64") + if err != nil { + t.Errorf("should not return error if valid key") + } + + if i != 1.2 { + t.Errorf("should return proper property") + } +} + +func TestFeaturePropertyString(t *testing.T) { + f := propertiesTestFeature() + + _, err := f.PropertyString("random") + if err == nil { + t.Errorf("should return error if invalid key") + } + + s, err := f.PropertyString("string") + if err != nil { + t.Errorf("should not return error if valid key") + } + + if s != "text" { + t.Errorf("should return proper property") + } +} + +func TestFeaturePropertyMustBool(t *testing.T) { + f := propertiesTestFeature() + + b := f.PropertyMustBool("random", true) + if b != true { + t.Errorf("should return default if property doesn't exist") + } + + b = f.PropertyMustBool("falsebool", true) + if b != false { + t.Errorf("should return proper property, with default") + } + + b = f.PropertyMustBool("falsebool") + if b != false { + t.Errorf("should return proper property, without default") + } +} + +func TestFeaturePropertyMustInt(t *testing.T) { + f := propertiesTestFeature() + + i := f.PropertyMustInt("random", 10) + if i != 10 { + t.Errorf("should return default if property doesn't exist") + } + + i = f.PropertyMustInt("int", 10) + if i != 1 { + t.Errorf("should return proper property, with default") + } + + i = f.PropertyMustInt("int") + if i != 1 { + t.Errorf("should return proper property, without default") + } +} + +func TestFeaturePropertyMustFloat64(t *testing.T) { + f := propertiesTestFeature() + + i := f.PropertyMustFloat64("random", 10) + if i != 10 { + t.Errorf("should return default if property doesn't exist") + } + + i = f.PropertyMustFloat64("float64", 10.0) + if i != 1.2 { + t.Errorf("should return proper property, with default") + } + + i = f.PropertyMustFloat64("float64") + if i != 1.2 { + t.Errorf("should return proper property, without default") + } +} + +func TestFeaturePropertyMustString(t *testing.T) { + f := propertiesTestFeature() + + s := f.PropertyMustString("random", "something") + if s != "something" { + t.Errorf("should return default if property doesn't exist") + } + + s = f.PropertyMustString("string", "something") + if s != "text" { + t.Errorf("should return proper property, with default") + } + + s = f.PropertyMustString("string") + if s != "text" { + t.Errorf("should return proper property, without default") + } +}