├── .github └── workflows │ └── main.yml ├── .travis.yml ├── LICENSE ├── README.md ├── boundingbox.go ├── example_test.go ├── feature.go ├── feature_collection.go ├── feature_collection_test.go ├── feature_test.go ├── geometry.go ├── geometry_test.go ├── go.mod ├── properties.go └── properties_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: '1.19' 19 | 20 | - name: Run build 21 | run: go build . 22 | 23 | - name: Run vet & lint 24 | run: | 25 | go vet . 26 | 27 | - name: Run tests 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - tip 6 | 7 | after_script: 8 | - FIXED=$(go vet ./... | wc -l); if [ $FIXED -gt 0 ]; then echo "go vet - $FIXED issues(s), please fix." && exit 2; fi 9 | - 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 10 | 11 | script: 12 | - go test -v 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go.geojson [![CI](https://github.com/paulmach/go.geojson/workflows/CI/badge.svg)](https://github.com/paulmach/go.geojson/actions?query=workflow%3ACI+event%3Apush) [![Godoc Reference](https://godoc.org/github.com/paulmach/go.geojson?status.svg)](https://godoc.org/github.com/paulmach/go.geojson) 2 | 3 | Go.geojson is a package for **encoding and decoding** [GeoJSON](http://geojson.org/) into Go structs. 4 | Supports both the [json.Marshaler](http://golang.org/pkg/encoding/json/#Marshaler) and [json.Unmarshaler](http://golang.org/pkg/encoding/json/#Unmarshaler) 5 | interfaces as well as [sql.Scanner](http://golang.org/pkg/database/sql/#Scanner) for directly scanning PostGIS query results. 6 | The package also provides helper functions such as `UnmarshalFeatureCollection`, `UnmarshalFeature` and `UnmarshalGeometry`. 7 | 8 | ### Deprecated, use [orb/geojson](https://github.com/paulmach/orb/tree/master/geojson) 9 | The [orb](https://github.com/paulmach/orb) package, and its subpackages, provide all the features here and more. 10 | 11 | ## Examples 12 | 13 | * #### Unmarshalling (JSON -> Go) 14 | 15 | 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`. 16 | 17 | // Feature Collection 18 | rawFeatureJSON := []byte(` 19 | { "type": "FeatureCollection", 20 | "features": [ 21 | { "type": "Feature", 22 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, 23 | "properties": {"prop0": "value0"} 24 | } 25 | ] 26 | }`) 27 | 28 | fc1, err := geojson.UnmarshalFeatureCollection(rawFeatureJSON) 29 | 30 | fc2 := geojson.NewFeatureCollection() 31 | err := json.Unmarshal(rawJSON, fc2) 32 | 33 | // Geometry 34 | rawGeometryJSON := []byte(`{"type": "Point", "coordinates": [102.0, 0.5]}`) 35 | g, err := geojson.UnmarshalGeometry(rawGeometryJSON) 36 | 37 | g.IsPoint() == true 38 | g.Point == []float64{102.0, 0.5} 39 | 40 | * #### Marshalling (Go -> JSON) 41 | 42 | g := geojson.NewPointGeometry([]float64{1, 2}) 43 | rawJSON, err := g.MarshalJSON() 44 | 45 | fc := geojson.NewFeatureCollection() 46 | fc.AddFeature(geojson.NewPointFeature([]float64{1,2})) 47 | rawJSON, err := fc.MarshalJSON() 48 | 49 | * #### Scanning PostGIS query results 50 | 51 | row := db.QueryRow("SELECT ST_AsGeoJSON(the_geom) FROM postgis_table) 52 | 53 | var geometry *geojson.Geometry 54 | row.Scan(&geometry) 55 | 56 | * #### Dealing with different Geometry types 57 | 58 | A geometry can be of several types, causing problems in a statically typed language. 59 | Thus there is a separate attribute on Geometry for each type. 60 | See the [Geometry object](https://godoc.org/github.com/paulmach/go.geojson#Geometry) for more details. 61 | 62 | g := geojson.UnmarshalGeometry([]byte(` 63 | { 64 | "type": "LineString", 65 | "coordinates": [ 66 | [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] 67 | ] 68 | }`)) 69 | 70 | switch { 71 | case g.IsPoint(): 72 | // do something with g.Point 73 | case g.IsLineString(): 74 | // do something with g.LineString 75 | } 76 | 77 | ## Feature Properties 78 | 79 | GeoJSON [Features](http://geojson.org/geojson-spec.html#feature-objects) can have properties of any type. 80 | This can cause issues in a statically typed language such as Go. 81 | So, included are some helper methods on the Feature object to ease the pain. 82 | 83 | // functions to do the casting for you 84 | func (f Feature) PropertyBool(key string) (bool, error) { 85 | func (f Feature) PropertyInt(key string) (int, error) { 86 | func (f Feature) PropertyFloat64(key string) (float64, error) { 87 | func (f Feature) PropertyString(key string) (string, error) { 88 | 89 | // functions that hide the error and let you define default 90 | func (f Feature) PropertyMustBool(key string, def ...bool) bool { 91 | func (f Feature) PropertyMustInt(key string, def ...int) int { 92 | func (f Feature) PropertyMustFloat64(key string, def ...float64) float64 { 93 | func (f Feature) PropertyMustString(key string, def ...string) string { 94 | 95 | -------------------------------------------------------------------------------- /boundingbox.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import "fmt" 4 | 5 | func decodeBoundingBox(bb interface{}) ([]float64, error) { 6 | if bb == nil { 7 | return nil, nil 8 | } 9 | 10 | switch f := bb.(type) { 11 | case []float64: 12 | return f, nil 13 | case []interface{}: 14 | bb := make([]float64, 0, 4) 15 | for _, v := range f { 16 | switch c := v.(type) { 17 | case float64: 18 | bb = append(bb, c) 19 | default: 20 | return nil, fmt.Errorf("bounding box coordinate not usable, got %T", v) 21 | } 22 | 23 | } 24 | return bb, nil 25 | default: 26 | return nil, fmt.Errorf("bounding box property not usable, got %T", bb) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package geojson_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | geojson "github.com/paulmach/go.geojson" 7 | ) 8 | 9 | func ExampleUnmarshalFeatureCollection() { 10 | rawFeatureJSON := []byte(` 11 | { "type": "FeatureCollection", 12 | "features": [ 13 | { "type": "Feature", 14 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, 15 | "properties": {"prop0": "value0"} 16 | } 17 | ] 18 | }`) 19 | 20 | fc, err := geojson.UnmarshalFeatureCollection(rawFeatureJSON) 21 | if err != nil { 22 | fmt.Printf("error: %v", err) 23 | return 24 | } 25 | 26 | fmt.Printf("%s", fc.Features[0].Properties["prop0"]) 27 | // Output: value0 28 | } 29 | 30 | func ExampleUnmarshalGeometry() { 31 | rawGeometryJSON := []byte(`{"type": "Point", "coordinates": [102.0, 0.5]}`) 32 | g, err := geojson.UnmarshalGeometry(rawGeometryJSON) 33 | if err != nil { 34 | fmt.Printf("error: %v", err) 35 | return 36 | } 37 | 38 | fmt.Printf("%s", g.Type) 39 | // Output: Point 40 | } 41 | 42 | func ExampleFeatureCollection_MarshalJSON() { 43 | fc := geojson.NewFeatureCollection() 44 | fc.AddFeature(geojson.NewPointFeature([]float64{1, 2})) 45 | rawJSON, err := fc.MarshalJSON() 46 | if err != nil { 47 | fmt.Printf("error: %v", err) 48 | return 49 | } 50 | 51 | fmt.Printf("%s", string(rawJSON)) 52 | // Output: {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":{}}]} 53 | } 54 | -------------------------------------------------------------------------------- /feature.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // A Feature corresponds to GeoJSON feature object 8 | type Feature struct { 9 | ID interface{} `json:"id,omitempty"` 10 | Type string `json:"type"` 11 | BoundingBox []float64 `json:"bbox,omitempty"` 12 | Geometry *Geometry `json:"geometry"` 13 | Properties map[string]interface{} `json:"properties"` 14 | CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported 15 | } 16 | 17 | // NewFeature creates and initializes a GeoJSON feature given the required attributes. 18 | func NewFeature(geometry *Geometry) *Feature { 19 | return &Feature{ 20 | Type: "Feature", 21 | Geometry: geometry, 22 | Properties: make(map[string]interface{}), 23 | } 24 | } 25 | 26 | // NewPointFeature creates and initializes a GeoJSON feature with a point geometry using the given coordinate. 27 | func NewPointFeature(coordinate []float64) *Feature { 28 | return NewFeature(NewPointGeometry(coordinate)) 29 | } 30 | 31 | // NewMultiPointFeature creates and initializes a GeoJSON feature with a multi-point geometry using the given coordinates. 32 | func NewMultiPointFeature(coordinates ...[]float64) *Feature { 33 | return NewFeature(NewMultiPointGeometry(coordinates...)) 34 | } 35 | 36 | // NewLineStringFeature creates and initializes a GeoJSON feature with a line string geometry using the given coordinates. 37 | func NewLineStringFeature(coordinates [][]float64) *Feature { 38 | return NewFeature(NewLineStringGeometry(coordinates)) 39 | } 40 | 41 | // NewMultiLineStringFeature creates and initializes a GeoJSON feature with a multi-line string geometry using the given lines. 42 | func NewMultiLineStringFeature(lines ...[][]float64) *Feature { 43 | return NewFeature(NewMultiLineStringGeometry(lines...)) 44 | } 45 | 46 | // NewPolygonFeature creates and initializes a GeoJSON feature with a polygon geometry using the given polygon. 47 | func NewPolygonFeature(polygon [][][]float64) *Feature { 48 | return NewFeature(NewPolygonGeometry(polygon)) 49 | } 50 | 51 | // NewMultiPolygonFeature creates and initializes a GeoJSON feature with a multi-polygon geometry using the given polygons. 52 | func NewMultiPolygonFeature(polygons ...[][][]float64) *Feature { 53 | return NewFeature(NewMultiPolygonGeometry(polygons...)) 54 | } 55 | 56 | // NewCollectionFeature creates and initializes a GeoJSON feature with a geometry collection geometry using the given geometries. 57 | func NewCollectionFeature(geometries ...*Geometry) *Feature { 58 | return NewFeature(NewCollectionGeometry(geometries...)) 59 | } 60 | 61 | // MarshalJSON converts the feature object into the proper JSON. 62 | // It will handle the encoding of all the child geometries. 63 | // Alternately one can call json.Marshal(f) directly for the same result. 64 | func (f Feature) MarshalJSON() ([]byte, error) { 65 | type feature Feature 66 | 67 | fea := &feature{ 68 | ID: f.ID, 69 | Type: "Feature", 70 | Geometry: f.Geometry, 71 | } 72 | 73 | if f.BoundingBox != nil && len(f.BoundingBox) != 0 { 74 | fea.BoundingBox = f.BoundingBox 75 | } 76 | if f.Properties != nil && len(f.Properties) != 0 { 77 | fea.Properties = f.Properties 78 | } else { 79 | fea.Properties = make(map[string]interface{}) 80 | } 81 | if f.CRS != nil && len(f.CRS) != 0 { 82 | fea.CRS = f.CRS 83 | } 84 | 85 | return json.Marshal(fea) 86 | } 87 | 88 | // UnmarshalFeature decodes the data into a GeoJSON feature. 89 | // Alternately one can call json.Unmarshal(f) directly for the same result. 90 | func UnmarshalFeature(data []byte) (*Feature, error) { 91 | f := &Feature{} 92 | err := json.Unmarshal(data, f) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return f, nil 98 | } 99 | -------------------------------------------------------------------------------- /feature_collection.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package geojson is a library for encoding and decoding GeoJSON into Go structs. 3 | Supports both the json.Marshaler and json.Unmarshaler interfaces as well as helper functions 4 | such as `UnmarshalFeatureCollection`, `UnmarshalFeature` and `UnmarshalGeometry`. 5 | */ 6 | package geojson 7 | 8 | import ( 9 | "encoding/json" 10 | ) 11 | 12 | // A FeatureCollection correlates to a GeoJSON feature collection. 13 | type FeatureCollection struct { 14 | Type string `json:"type"` 15 | BoundingBox []float64 `json:"bbox,omitempty"` 16 | Features []*Feature `json:"features"` 17 | CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported 18 | } 19 | 20 | // NewFeatureCollection creates and initializes a new feature collection. 21 | func NewFeatureCollection() *FeatureCollection { 22 | return &FeatureCollection{ 23 | Type: "FeatureCollection", 24 | Features: make([]*Feature, 0), 25 | } 26 | } 27 | 28 | // AddFeature appends a feature to the collection. 29 | func (fc *FeatureCollection) AddFeature(feature *Feature) *FeatureCollection { 30 | fc.Features = append(fc.Features, feature) 31 | return fc 32 | } 33 | 34 | // MarshalJSON converts the feature collection object into the proper JSON. 35 | // It will handle the encoding of all the child features and geometries. 36 | // Alternately one can call json.Marshal(fc) directly for the same result. 37 | func (fc FeatureCollection) MarshalJSON() ([]byte, error) { 38 | type featureCollection FeatureCollection 39 | 40 | fcol := &featureCollection{ 41 | Type: "FeatureCollection", 42 | } 43 | 44 | if fc.BoundingBox != nil && len(fc.BoundingBox) != 0 { 45 | fcol.BoundingBox = fc.BoundingBox 46 | } 47 | 48 | fcol.Features = fc.Features 49 | if fcol.Features == nil { 50 | fcol.Features = make([]*Feature, 0) // GeoJSON requires the feature attribute to be at least [] 51 | } 52 | 53 | if fc.CRS != nil && len(fc.CRS) != 0 { 54 | fcol.CRS = fc.CRS 55 | } 56 | 57 | return json.Marshal(fcol) 58 | } 59 | 60 | // UnmarshalFeatureCollection decodes the data into a GeoJSON feature collection. 61 | // Alternately one can call json.Unmarshal(fc) directly for the same result. 62 | func UnmarshalFeatureCollection(data []byte) (*FeatureCollection, error) { 63 | fc := &FeatureCollection{} 64 | err := json.Unmarshal(data, fc) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return fc, nil 70 | } 71 | -------------------------------------------------------------------------------- /feature_collection_test.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestNewFeatureCollection(t *testing.T) { 10 | fc := NewFeatureCollection() 11 | 12 | if fc.Type != "FeatureCollection" { 13 | t.Errorf("should have type of FeatureCollection, got %v", fc.Type) 14 | } 15 | } 16 | 17 | func TestUnmarshalFeatureCollection(t *testing.T) { 18 | rawJSON := ` 19 | { "type": "FeatureCollection", 20 | "features": [ 21 | { "type": "Feature", 22 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, 23 | "properties": {"prop0": "value0"} 24 | }, 25 | { "type": "Feature", 26 | "geometry": { 27 | "type": "LineString", 28 | "coordinates": [ 29 | [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] 30 | ] 31 | }, 32 | "properties": { 33 | "prop0": "value0", 34 | "prop1": 0.0 35 | } 36 | }, 37 | { "type": "Feature", 38 | "geometry": { 39 | "type": "Polygon", 40 | "coordinates": [ 41 | [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], 42 | [100.0, 1.0], [100.0, 0.0] ] 43 | ] 44 | }, 45 | "properties": { 46 | "prop0": "value0", 47 | "prop1": {"this": "that"} 48 | } 49 | } 50 | ] 51 | }` 52 | 53 | fc, err := UnmarshalFeatureCollection([]byte(rawJSON)) 54 | if err != nil { 55 | t.Fatalf("should unmarshal feature collection without issue, err %v", err) 56 | } 57 | 58 | if fc.Type != "FeatureCollection" { 59 | t.Errorf("should have type of FeatureCollection, got %v", fc.Type) 60 | } 61 | 62 | if len(fc.Features) != 3 { 63 | t.Errorf("should have 3 features but got %d", len(fc.Features)) 64 | } 65 | } 66 | 67 | func TestFeatureCollectionMarshalJSON(t *testing.T) { 68 | fc := NewFeatureCollection() 69 | fc.Features = nil 70 | blob, err := fc.MarshalJSON() 71 | 72 | if err != nil { 73 | t.Fatalf("should marshal to json just fine but got %v", err) 74 | } 75 | 76 | if !bytes.Contains(blob, []byte(`"features":[]`)) { 77 | t.Errorf("json should set features object to at least empty array") 78 | } 79 | } 80 | 81 | func TestFeatureCollectionMarshal(t *testing.T) { 82 | fc := NewFeatureCollection() 83 | blob, err := json.Marshal(fc) 84 | fc.Features = nil 85 | 86 | if err != nil { 87 | t.Fatalf("should marshal to json just fine but got %v", err) 88 | } 89 | 90 | if !bytes.Contains(blob, []byte(`"features":[]`)) { 91 | t.Errorf("json should set features object to at least empty array") 92 | } 93 | } 94 | 95 | func TestFeatureCollectionMarshalValue(t *testing.T) { 96 | fc := NewFeatureCollection() 97 | fc.Features = nil 98 | blob, err := json.Marshal(*fc) 99 | 100 | if err != nil { 101 | t.Fatalf("should marshal to json just fine but got %v", err) 102 | } 103 | 104 | if !bytes.Contains(blob, []byte(`"features":[]`)) { 105 | t.Errorf("json should set features object to at least empty array") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /feature_test.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestNewFeature(t *testing.T) { 10 | f := NewFeature(NewPointGeometry([]float64{1, 2})) 11 | 12 | if f.Type != "Feature" { 13 | t.Errorf("should have type of Feature, got %v", f.Type) 14 | } 15 | } 16 | 17 | func TestFeatureMarshalJSON(t *testing.T) { 18 | f := NewFeature(NewPointGeometry([]float64{1, 2})) 19 | blob, err := f.MarshalJSON() 20 | 21 | if err != nil { 22 | t.Fatalf("should marshal to json just fine but got %v", err) 23 | } 24 | 25 | if !bytes.Contains(blob, []byte(`"properties":{}`)) { 26 | t.Errorf("json should set properties to empty object if there are none") 27 | } 28 | } 29 | 30 | func TestFeatureMarshal(t *testing.T) { 31 | f := NewFeature(NewPointGeometry([]float64{1, 2})) 32 | blob, err := json.Marshal(f) 33 | 34 | if err != nil { 35 | t.Fatalf("should marshal to json just fine but got %v", err) 36 | } 37 | 38 | if !bytes.Contains(blob, []byte(`"properties":{}`)) { 39 | t.Errorf("json should set properties to empty object if there are none") 40 | } 41 | } 42 | 43 | func TestFeatureMarshalValue(t *testing.T) { 44 | f := NewFeature(NewPointGeometry([]float64{1, 2})) 45 | blob, err := json.Marshal(*f) 46 | 47 | if err != nil { 48 | t.Fatalf("should marshal to json just fine but got %v", err) 49 | } 50 | 51 | if !bytes.Contains(blob, []byte(`"properties":{}`)) { 52 | t.Errorf("json should set properties to empty object if there are none") 53 | } 54 | } 55 | 56 | func TestUnmarshalFeature(t *testing.T) { 57 | rawJSON := ` 58 | { "type": "Feature", 59 | "bbox": [1, 2, 3, 4], 60 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, 61 | "properties": {"prop0": "value0"} 62 | }` 63 | 64 | f, err := UnmarshalFeature([]byte(rawJSON)) 65 | if err != nil { 66 | t.Fatalf("should unmarshal feature without issue, err %v", err) 67 | } 68 | 69 | if f.Type != "Feature" { 70 | t.Errorf("should have type of Feature, got %v", f.Type) 71 | } 72 | 73 | if len(f.Properties) != 1 { 74 | t.Errorf("should have 1 property but got %d", len(f.Properties)) 75 | } 76 | 77 | if len(f.BoundingBox) != 4 { 78 | t.Errorf("should have unmarshalled bounding box") 79 | } 80 | } 81 | 82 | func TestMarshalFeatureID(t *testing.T) { 83 | f := &Feature{ 84 | ID: "asdf", 85 | } 86 | 87 | data, err := f.MarshalJSON() 88 | if err != nil { 89 | t.Fatalf("should marshal, %v", err) 90 | } 91 | 92 | if !bytes.Equal(data, []byte(`{"id":"asdf","type":"Feature","geometry":null,"properties":{}}`)) { 93 | t.Errorf("data not correct") 94 | t.Logf("%v", string(data)) 95 | } 96 | 97 | f.ID = 123 98 | data, err = f.MarshalJSON() 99 | if err != nil { 100 | t.Fatalf("should marshal, %v", err) 101 | 102 | } 103 | 104 | if !bytes.Equal(data, []byte(`{"id":123,"type":"Feature","geometry":null,"properties":{}}`)) { 105 | t.Errorf("data not correct") 106 | t.Logf("%v", string(data)) 107 | } 108 | } 109 | 110 | func TestUnmarshalFeatureID(t *testing.T) { 111 | rawJSON := ` 112 | { "type": "Feature", 113 | "id": 123, 114 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]} 115 | }` 116 | 117 | f, err := UnmarshalFeature([]byte(rawJSON)) 118 | if err != nil { 119 | t.Fatalf("should unmarshal feature without issue, err %v", err) 120 | } 121 | 122 | if v, ok := f.ID.(float64); !ok || v != 123 { 123 | t.Errorf("should parse id as number, got %T %f", f.ID, v) 124 | } 125 | 126 | rawJSON = ` 127 | { "type": "Feature", 128 | "id": "abcd", 129 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]} 130 | }` 131 | 132 | f, err = UnmarshalFeature([]byte(rawJSON)) 133 | if err != nil { 134 | t.Fatalf("should unmarshal feature without issue, err %v", err) 135 | } 136 | 137 | if v, ok := f.ID.(string); !ok || v != "abcd" { 138 | t.Errorf("should parse id as string, got %T %s", f.ID, v) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /geometry.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | // A GeometryType serves to enumerate the different GeoJSON geometry types. 11 | type GeometryType string 12 | 13 | // The geometry types supported by GeoJSON 1.0 14 | const ( 15 | GeometryPoint GeometryType = "Point" 16 | GeometryMultiPoint GeometryType = "MultiPoint" 17 | GeometryLineString GeometryType = "LineString" 18 | GeometryMultiLineString GeometryType = "MultiLineString" 19 | GeometryPolygon GeometryType = "Polygon" 20 | GeometryMultiPolygon GeometryType = "MultiPolygon" 21 | GeometryCollection GeometryType = "GeometryCollection" 22 | ) 23 | 24 | // A Geometry correlates to a GeoJSON geometry object. 25 | type Geometry struct { 26 | Type GeometryType `json:"type"` 27 | BoundingBox []float64 `json:"bbox,omitempty"` 28 | Point []float64 29 | MultiPoint [][]float64 30 | LineString [][]float64 31 | MultiLineString [][][]float64 32 | Polygon [][][]float64 33 | MultiPolygon [][][][]float64 34 | Geometries []*Geometry 35 | CRS map[string]interface{} `json:"crs,omitempty"` // Coordinate Reference System Objects are not currently supported 36 | } 37 | 38 | // NewPointGeometry creates and initializes a point geometry with the give coordinate. 39 | func NewPointGeometry(coordinate []float64) *Geometry { 40 | return &Geometry{ 41 | Type: GeometryPoint, 42 | Point: coordinate, 43 | } 44 | } 45 | 46 | // NewMultiPointGeometry creates and initializes a multi-point geometry with the given coordinates. 47 | func NewMultiPointGeometry(coordinates ...[]float64) *Geometry { 48 | return &Geometry{ 49 | Type: GeometryMultiPoint, 50 | MultiPoint: coordinates, 51 | } 52 | } 53 | 54 | // NewLineStringGeometry creates and initializes a line string geometry with the given coordinates. 55 | func NewLineStringGeometry(coordinates [][]float64) *Geometry { 56 | return &Geometry{ 57 | Type: GeometryLineString, 58 | LineString: coordinates, 59 | } 60 | } 61 | 62 | // NewMultiLineStringGeometry creates and initializes a multi-line string geometry with the given lines. 63 | func NewMultiLineStringGeometry(lines ...[][]float64) *Geometry { 64 | return &Geometry{ 65 | Type: GeometryMultiLineString, 66 | MultiLineString: lines, 67 | } 68 | } 69 | 70 | // NewPolygonGeometry creates and initializes a polygon geometry with the given polygon. 71 | func NewPolygonGeometry(polygon [][][]float64) *Geometry { 72 | return &Geometry{ 73 | Type: GeometryPolygon, 74 | Polygon: polygon, 75 | } 76 | } 77 | 78 | // NewMultiPolygonGeometry creates and initializes a multi-polygon geometry with the given polygons. 79 | func NewMultiPolygonGeometry(polygons ...[][][]float64) *Geometry { 80 | return &Geometry{ 81 | Type: GeometryMultiPolygon, 82 | MultiPolygon: polygons, 83 | } 84 | } 85 | 86 | // NewCollectionGeometry creates and initializes a geometry collection geometry with the given geometries. 87 | func NewCollectionGeometry(geometries ...*Geometry) *Geometry { 88 | return &Geometry{ 89 | Type: GeometryCollection, 90 | Geometries: geometries, 91 | } 92 | } 93 | 94 | // MarshalJSON converts the geometry object into the correct JSON. 95 | // This fulfills the json.Marshaler interface. 96 | func (g Geometry) MarshalJSON() ([]byte, error) { 97 | // defining a struct here lets us define the order of the JSON elements. 98 | type geometry struct { 99 | Type GeometryType `json:"type"` 100 | BoundingBox []float64 `json:"bbox,omitempty"` 101 | Coordinates interface{} `json:"coordinates,omitempty"` 102 | Geometries interface{} `json:"geometries,omitempty"` 103 | CRS map[string]interface{} `json:"crs,omitempty"` 104 | } 105 | 106 | geo := &geometry{ 107 | Type: g.Type, 108 | } 109 | 110 | if g.BoundingBox != nil && len(g.BoundingBox) != 0 { 111 | geo.BoundingBox = g.BoundingBox 112 | } 113 | 114 | switch g.Type { 115 | case GeometryPoint: 116 | geo.Coordinates = g.Point 117 | case GeometryMultiPoint: 118 | geo.Coordinates = g.MultiPoint 119 | case GeometryLineString: 120 | geo.Coordinates = g.LineString 121 | case GeometryMultiLineString: 122 | geo.Coordinates = g.MultiLineString 123 | case GeometryPolygon: 124 | geo.Coordinates = g.Polygon 125 | case GeometryMultiPolygon: 126 | geo.Coordinates = g.MultiPolygon 127 | case GeometryCollection: 128 | geo.Geometries = g.Geometries 129 | } 130 | 131 | return json.Marshal(geo) 132 | } 133 | 134 | // UnmarshalGeometry decodes the data into a GeoJSON geometry. 135 | // Alternately one can call json.Unmarshal(g) directly for the same result. 136 | func UnmarshalGeometry(data []byte) (*Geometry, error) { 137 | g := &Geometry{} 138 | err := json.Unmarshal(data, g) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | return g, nil 144 | } 145 | 146 | // UnmarshalJSON decodes the data into a GeoJSON geometry. 147 | // This fulfills the json.Unmarshaler interface. 148 | func (g *Geometry) UnmarshalJSON(data []byte) error { 149 | var object map[string]interface{} 150 | err := json.Unmarshal(data, &object) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | return decodeGeometry(g, object) 156 | } 157 | 158 | // Scan implements the sql.Scanner interface allowing 159 | // geometry structs to be passed into rows.Scan(...interface{}) 160 | // The columns must be received as GeoJSON Geometry. 161 | // When using PostGIS a spatial column would need to be wrapped in ST_AsGeoJSON. 162 | func (g *Geometry) Scan(value interface{}) error { 163 | var data []byte 164 | 165 | switch value.(type) { 166 | case string: 167 | data = []byte(value.(string)) 168 | case []byte: 169 | data = value.([]byte) 170 | default: 171 | return errors.New("unable to parse this type into geojson") 172 | } 173 | 174 | return g.UnmarshalJSON(data) 175 | } 176 | 177 | // Value implements the driver Valuer interface. 178 | // The columns must be sending as GeoJSON Geometry. 179 | // When using PostGIS a spatial column would need to be wrapped in ST_GeomFromGeoJSON. 180 | func (g *Geometry) Value() (driver.Value, error) { 181 | return g.MarshalJSON() 182 | } 183 | 184 | func decodeGeometry(g *Geometry, object map[string]interface{}) error { 185 | t, ok := object["type"] 186 | if !ok { 187 | return errors.New("type property not defined") 188 | } 189 | 190 | if s, ok := t.(string); ok { 191 | g.Type = GeometryType(s) 192 | } else { 193 | return errors.New("type property not string") 194 | } 195 | 196 | bb, err := decodeBoundingBox(object["bbox"]) 197 | if err != nil { 198 | return err 199 | } 200 | g.BoundingBox = bb 201 | 202 | switch g.Type { 203 | case GeometryPoint: 204 | g.Point, err = decodePosition(object["coordinates"]) 205 | case GeometryMultiPoint: 206 | g.MultiPoint, err = decodePositionSet(object["coordinates"]) 207 | case GeometryLineString: 208 | g.LineString, err = decodePositionSet(object["coordinates"]) 209 | case GeometryMultiLineString: 210 | g.MultiLineString, err = decodePathSet(object["coordinates"]) 211 | case GeometryPolygon: 212 | g.Polygon, err = decodePathSet(object["coordinates"]) 213 | case GeometryMultiPolygon: 214 | g.MultiPolygon, err = decodePolygonSet(object["coordinates"]) 215 | case GeometryCollection: 216 | g.Geometries, err = decodeGeometries(object["geometries"]) 217 | } 218 | 219 | return err 220 | } 221 | 222 | func decodePosition(data interface{}) ([]float64, error) { 223 | coords, ok := data.([]interface{}) 224 | if !ok { 225 | return nil, fmt.Errorf("not a valid position, got %v", data) 226 | } 227 | 228 | result := make([]float64, 0, len(coords)) 229 | for _, coord := range coords { 230 | if f, ok := coord.(float64); ok { 231 | result = append(result, f) 232 | } else { 233 | return nil, fmt.Errorf("not a valid coordinate, got %v", coord) 234 | } 235 | } 236 | 237 | return result, nil 238 | } 239 | 240 | func decodePositionSet(data interface{}) ([][]float64, error) { 241 | points, ok := data.([]interface{}) 242 | if !ok { 243 | return nil, fmt.Errorf("not a valid set of positions, got %v", data) 244 | } 245 | 246 | result := make([][]float64, 0, len(points)) 247 | for _, point := range points { 248 | if p, err := decodePosition(point); err == nil { 249 | result = append(result, p) 250 | } else { 251 | return nil, err 252 | } 253 | } 254 | 255 | return result, nil 256 | } 257 | 258 | func decodePathSet(data interface{}) ([][][]float64, error) { 259 | sets, ok := data.([]interface{}) 260 | if !ok { 261 | return nil, fmt.Errorf("not a valid path, got %v", data) 262 | } 263 | 264 | result := make([][][]float64, 0, len(sets)) 265 | 266 | for _, set := range sets { 267 | if s, err := decodePositionSet(set); err == nil { 268 | result = append(result, s) 269 | } else { 270 | return nil, err 271 | } 272 | } 273 | 274 | return result, nil 275 | } 276 | 277 | func decodePolygonSet(data interface{}) ([][][][]float64, error) { 278 | polygons, ok := data.([]interface{}) 279 | if !ok { 280 | return nil, fmt.Errorf("not a valid polygon, got %v", data) 281 | } 282 | 283 | result := make([][][][]float64, 0, len(polygons)) 284 | for _, polygon := range polygons { 285 | if p, err := decodePathSet(polygon); err == nil { 286 | result = append(result, p) 287 | } else { 288 | return nil, err 289 | } 290 | } 291 | 292 | return result, nil 293 | } 294 | 295 | func decodeGeometries(data interface{}) ([]*Geometry, error) { 296 | if vs, ok := data.([]interface{}); ok { 297 | geometries := make([]*Geometry, 0, len(vs)) 298 | for _, v := range vs { 299 | g := &Geometry{} 300 | 301 | vmap, ok := v.(map[string]interface{}) 302 | if !ok { 303 | break 304 | } 305 | 306 | err := decodeGeometry(g, vmap) 307 | if err != nil { 308 | return nil, err 309 | } 310 | 311 | geometries = append(geometries, g) 312 | } 313 | 314 | if len(geometries) == len(vs) { 315 | return geometries, nil 316 | } 317 | } 318 | 319 | return nil, fmt.Errorf("not a valid set of geometries, got %v", data) 320 | } 321 | 322 | // IsPoint returns true with the geometry object is a Point type. 323 | func (g *Geometry) IsPoint() bool { 324 | return g.Type == GeometryPoint 325 | } 326 | 327 | // IsMultiPoint returns true with the geometry object is a MultiPoint type. 328 | func (g *Geometry) IsMultiPoint() bool { 329 | return g.Type == GeometryMultiPoint 330 | } 331 | 332 | // IsLineString returns true with the geometry object is a LineString type. 333 | func (g *Geometry) IsLineString() bool { 334 | return g.Type == GeometryLineString 335 | } 336 | 337 | // IsMultiLineString returns true with the geometry object is a LineString type. 338 | func (g *Geometry) IsMultiLineString() bool { 339 | return g.Type == GeometryMultiLineString 340 | } 341 | 342 | // IsPolygon returns true with the geometry object is a Polygon type. 343 | func (g *Geometry) IsPolygon() bool { 344 | return g.Type == GeometryPolygon 345 | } 346 | 347 | // IsMultiPolygon returns true with the geometry object is a MultiPolygon type. 348 | func (g *Geometry) IsMultiPolygon() bool { 349 | return g.Type == GeometryMultiPolygon 350 | } 351 | 352 | // IsCollection returns true with the geometry object is a GeometryCollection type. 353 | func (g *Geometry) IsCollection() bool { 354 | return g.Type == GeometryCollection 355 | } 356 | -------------------------------------------------------------------------------- /geometry_test.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestGeometryMarshalJSONPoint(t *testing.T) { 10 | g := NewPointGeometry([]float64{1, 2}) 11 | blob, err := g.MarshalJSON() 12 | 13 | if err != nil { 14 | t.Fatalf("should marshal to json just fine but got %v", err) 15 | } 16 | 17 | if !bytes.Contains(blob, []byte(`"type":"Point"`)) { 18 | t.Errorf("json should have type Point") 19 | } 20 | 21 | if !bytes.Contains(blob, []byte(`"coordinates":[1,2]`)) { 22 | t.Errorf("json should marshal coordinates correctly") 23 | } 24 | } 25 | 26 | func TestGeometryMarshalPoint(t *testing.T) { 27 | g := NewPointGeometry([]float64{1, 2}) 28 | blob, err := json.Marshal(g) 29 | 30 | if err != nil { 31 | t.Fatalf("should json.Marshal just fine but got %v", err) 32 | } 33 | 34 | if !bytes.Contains(blob, []byte(`"type":"Point"`)) { 35 | t.Errorf("json should have type Point") 36 | } 37 | 38 | if !bytes.Contains(blob, []byte(`"coordinates":[1,2]`)) { 39 | t.Errorf("json should marshal coordinates correctly") 40 | } 41 | } 42 | 43 | func TestGeometryMarshalPointValue(t *testing.T) { 44 | g := NewPointGeometry([]float64{1, 2}) 45 | blob, err := json.Marshal(*g) 46 | 47 | if err != nil { 48 | t.Fatalf("should json.Marshal just fine but got %v", err) 49 | } 50 | 51 | if !bytes.Contains(blob, []byte(`"type":"Point"`)) { 52 | t.Errorf("json should have type Point") 53 | } 54 | 55 | if !bytes.Contains(blob, []byte(`"coordinates":[1,2]`)) { 56 | t.Errorf("json should marshal coordinates correctly") 57 | } 58 | } 59 | 60 | func TestGeometryMarshalJSONMultiPoint(t *testing.T) { 61 | g := NewMultiPointGeometry([]float64{1, 2}, []float64{3, 4}) 62 | blob, err := g.MarshalJSON() 63 | 64 | if err != nil { 65 | t.Fatalf("should marshal to json just fine but got %v", err) 66 | } 67 | 68 | if !bytes.Contains(blob, []byte(`"type":"MultiPoint"`)) { 69 | t.Errorf("json should have type MultiPoint") 70 | } 71 | 72 | if !bytes.Contains(blob, []byte(`"coordinates":[[1,2],[3,4]]`)) { 73 | t.Errorf("json should marshal coordinates correctly") 74 | } 75 | } 76 | 77 | func TestGeometryMarshalJSONLineString(t *testing.T) { 78 | g := NewLineStringGeometry([][]float64{{1, 2}, {3, 4}}) 79 | blob, err := g.MarshalJSON() 80 | 81 | if err != nil { 82 | t.Fatalf("should marshal to json just fine but got %v", err) 83 | } 84 | 85 | if !bytes.Contains(blob, []byte(`"type":"LineString"`)) { 86 | t.Errorf("json should have type LineString") 87 | } 88 | 89 | if !bytes.Contains(blob, []byte(`"coordinates":[[1,2],[3,4]]`)) { 90 | t.Errorf("json should marshal coordinates correctly") 91 | } 92 | } 93 | 94 | func TestGeometryMarshalJSONMultiLineString(t *testing.T) { 95 | g := NewMultiLineStringGeometry( 96 | [][]float64{{1, 2}, {3, 4}}, 97 | [][]float64{{5, 6}, {7, 8}}, 98 | ) 99 | blob, err := g.MarshalJSON() 100 | 101 | if err != nil { 102 | t.Fatalf("should marshal to json just fine but got %v", err) 103 | } 104 | 105 | if !bytes.Contains(blob, []byte(`"type":"MultiLineString"`)) { 106 | t.Errorf("json should have type MultiLineString") 107 | } 108 | 109 | if !bytes.Contains(blob, []byte(`"coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]`)) { 110 | t.Errorf("json should marshal coordinates correctly") 111 | } 112 | } 113 | 114 | func TestGeometryMarshalJSONPolygon(t *testing.T) { 115 | g := NewPolygonGeometry([][][]float64{ 116 | {{1, 2}, {3, 4}}, 117 | {{5, 6}, {7, 8}}, 118 | }) 119 | blob, err := g.MarshalJSON() 120 | 121 | if err != nil { 122 | t.Fatalf("should marshal to json just fine but got %v", err) 123 | } 124 | 125 | if !bytes.Contains(blob, []byte(`"type":"Polygon"`)) { 126 | t.Errorf("json should have type Polygon") 127 | } 128 | 129 | if !bytes.Contains(blob, []byte(`"coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]`)) { 130 | t.Errorf("json should marshal coordinates correctly") 131 | } 132 | } 133 | 134 | func TestGeometryMarshalJSONMultiPolygon(t *testing.T) { 135 | g := NewMultiPolygonGeometry( 136 | [][][]float64{ 137 | {{1, 2}, {3, 4}}, 138 | {{5, 6}, {7, 8}}, 139 | }, 140 | [][][]float64{ 141 | {{8, 7}, {6, 5}}, 142 | {{4, 3}, {2, 1}}, 143 | }) 144 | blob, err := g.MarshalJSON() 145 | 146 | if err != nil { 147 | t.Fatalf("should marshal to json just fine but got %v", err) 148 | } 149 | 150 | if !bytes.Contains(blob, []byte(`"type":"MultiPolygon"`)) { 151 | t.Errorf("json should have type MultiPolygon") 152 | } 153 | 154 | if !bytes.Contains(blob, []byte(`"coordinates":[[[[1,2],[3,4]],[[5,6],[7,8]]],[[[8,7],[6,5]],[[4,3],[2,1]]]]`)) { 155 | t.Errorf("json should marshal coordinates correctly") 156 | } 157 | } 158 | 159 | func TestGeometryMarshalJSONCollection(t *testing.T) { 160 | g := NewCollectionGeometry( 161 | NewPointGeometry([]float64{1, 2}), 162 | NewMultiPointGeometry([]float64{1, 2}, []float64{3, 4}), 163 | ) 164 | blob, err := g.MarshalJSON() 165 | 166 | if err != nil { 167 | t.Fatalf("should marshal to json just fine but got %v", err) 168 | } 169 | 170 | if !bytes.Contains(blob, []byte(`"type":"GeometryCollection"`)) { 171 | t.Errorf("json should have type GeometryCollection") 172 | } 173 | 174 | if !bytes.Contains(blob, []byte(`"geometries":`)) { 175 | t.Errorf("json should have geometries attribute") 176 | } 177 | } 178 | 179 | func TestUnmarshalGeometryPoint(t *testing.T) { 180 | rawJSON := `{"type": "Point", "coordinates": [102.0, 0.5]}` 181 | 182 | g, err := UnmarshalGeometry([]byte(rawJSON)) 183 | if err != nil { 184 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 185 | } 186 | 187 | if g.Type != "Point" { 188 | t.Errorf("incorrect type, got %v", g.Type) 189 | } 190 | 191 | if len(g.Point) != 2 { 192 | t.Errorf("should have 2 coordinate elements but got %d", len(g.Point)) 193 | } 194 | } 195 | 196 | func TestUnmarshalGeometryMultiPoint(t *testing.T) { 197 | rawJSON := `{"type": "MultiPoint", "coordinates": [[1,2],[3,4]]}` 198 | 199 | g, err := UnmarshalGeometry([]byte(rawJSON)) 200 | if err != nil { 201 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 202 | } 203 | 204 | if g.Type != "MultiPoint" { 205 | t.Errorf("incorrect type, got %v", g.Type) 206 | } 207 | 208 | if len(g.MultiPoint) != 2 { 209 | t.Errorf("should have 2 coordinate elements but got %d", len(g.MultiPoint)) 210 | } 211 | } 212 | 213 | func TestUnmarshalGeometryLineString(t *testing.T) { 214 | rawJSON := `{"type": "LineString", "coordinates": [[1,2],[3,4]]}` 215 | 216 | g, err := UnmarshalGeometry([]byte(rawJSON)) 217 | if err != nil { 218 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 219 | } 220 | 221 | if g.Type != "LineString" { 222 | t.Errorf("incorrect type, got %v", g.Type) 223 | } 224 | 225 | if len(g.LineString) != 2 { 226 | t.Errorf("should have 2 line string coordinates but got %d", len(g.LineString)) 227 | } 228 | } 229 | 230 | func TestUnmarshalGeometryMultiLineString(t *testing.T) { 231 | rawJSON := `{"type": "MultiLineString", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]}` 232 | 233 | g, err := UnmarshalGeometry([]byte(rawJSON)) 234 | if err != nil { 235 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 236 | } 237 | 238 | if g.Type != "MultiLineString" { 239 | t.Errorf("incorrect type, got %v", g.Type) 240 | } 241 | 242 | if len(g.MultiLineString) != 2 { 243 | t.Errorf("should have 2 line strings but got %d", len(g.MultiLineString)) 244 | } 245 | } 246 | 247 | func TestUnmarshalGeometryPolygon(t *testing.T) { 248 | rawJSON := `{"type": "Polygon", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]}` 249 | 250 | g, err := UnmarshalGeometry([]byte(rawJSON)) 251 | if err != nil { 252 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 253 | } 254 | 255 | if g.Type != "Polygon" { 256 | t.Errorf("incorrect type, got %v", g.Type) 257 | } 258 | 259 | if len(g.Polygon) != 2 { 260 | t.Errorf("should have 2 polygon paths but got %d", len(g.Polygon)) 261 | } 262 | } 263 | 264 | func TestUnmarshalGeometryPolygonBoundingBox(t *testing.T) { 265 | rawJSON := `{"type": "Polygon", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]], "bbox": [1,2,7,8]}` 266 | 267 | g, err := UnmarshalGeometry([]byte(rawJSON)) 268 | if err != nil { 269 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 270 | } 271 | 272 | if g.Type != "Polygon" { 273 | t.Errorf("incorrect type, got %v", g.Type) 274 | } 275 | 276 | if len(g.Polygon) != 2 { 277 | t.Errorf("should have 2 polygon paths but got %d", len(g.Polygon)) 278 | } 279 | if len(g.BoundingBox) != 4 { 280 | t.Errorf("should have unmarshalled bounding box") 281 | } 282 | } 283 | 284 | func TestUnmarshalGeometryMultiPolygon(t *testing.T) { 285 | rawJSON := `{"type": "MultiPolygon", "coordinates": [[[[1,2],[3,4]],[[5,6],[7,8]]],[[[8,7],[6,5]],[[4,3],[2,1]]]]}` 286 | 287 | g, err := UnmarshalGeometry([]byte(rawJSON)) 288 | if err != nil { 289 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 290 | } 291 | 292 | if g.Type != "MultiPolygon" { 293 | t.Errorf("incorrect type, got %v", g.Type) 294 | } 295 | 296 | if len(g.MultiPolygon) != 2 { 297 | t.Errorf("should have 2 polygons but got %d", len(g.MultiPolygon)) 298 | } 299 | } 300 | 301 | func TestUnmarshalGeometryCollection(t *testing.T) { 302 | rawJSON := `{"type": "GeometryCollection", "geometries": [ 303 | {"type": "Point", "coordinates": [102.0, 0.5]}, 304 | {"type": "MultiLineString", "coordinates": [[[1,2],[3,4]],[[5,6],[7,8]]]} 305 | ]}` 306 | 307 | g, err := UnmarshalGeometry([]byte(rawJSON)) 308 | if err != nil { 309 | t.Fatalf("should unmarshal geometry without issue, err %v", err) 310 | } 311 | 312 | if g.Type != "GeometryCollection" { 313 | t.Errorf("incorrect type, got %v", g.Type) 314 | } 315 | 316 | if len(g.Geometries) != 2 { 317 | t.Errorf("should have 2 geometries but got %d", len(g.Geometries)) 318 | } 319 | } 320 | 321 | func TestGeometryScanFail(t *testing.T) { 322 | g := &Geometry{} 323 | 324 | err := g.Scan(123) 325 | if err == nil { 326 | t.Errorf("should return error if not the correct data type") 327 | } 328 | } 329 | 330 | func TestGeometryScan(t *testing.T) { 331 | cases := []struct { 332 | name string 333 | value interface{} 334 | }{ 335 | { 336 | name: "Scan from bytes", 337 | value: []byte(`{"type":"Point","coordinates":[-93.787988,32.392335]}`), 338 | }, 339 | { 340 | name: "Scan from string", 341 | value: `{"type":"Point","coordinates":[-93.787988,32.392335]}`, 342 | }, 343 | } 344 | 345 | for _, tc := range cases { 346 | t.Run(tc.name, func(t *testing.T) { 347 | g := &Geometry{} 348 | 349 | err := g.Scan(tc.value) 350 | if err != nil { 351 | t.Fatalf("should parse without error, got %v", err) 352 | } 353 | 354 | if !g.IsPoint() { 355 | t.Errorf("should be point, but got %v", g) 356 | } 357 | 358 | if g.Point[0] != -93.787988 || g.Point[1] != 32.392335 { 359 | t.Errorf("incorrect point data, got %v", g.Point) 360 | } 361 | }) 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paulmach/go.geojson 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /properties.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // SetProperty provides the inverse of all the property functions 8 | // and is here for consistency. 9 | func (f *Feature) SetProperty(key string, value interface{}) { 10 | if f.Properties == nil { 11 | f.Properties = make(map[string]interface{}) 12 | } 13 | f.Properties[key] = value 14 | } 15 | 16 | // PropertyBool type asserts a property to `bool`. 17 | func (f *Feature) PropertyBool(key string) (bool, error) { 18 | if b, ok := (f.Properties[key]).(bool); ok { 19 | return b, nil 20 | } 21 | return false, fmt.Errorf("type assertion of `%s` to bool failed", key) 22 | } 23 | 24 | // PropertyInt type asserts a property to `int`. 25 | func (f *Feature) PropertyInt(key string) (int, error) { 26 | if i, ok := (f.Properties[key]).(int); ok { 27 | return i, nil 28 | } 29 | 30 | if i, ok := (f.Properties[key]).(float64); ok { 31 | return int(i), nil 32 | } 33 | 34 | return 0, fmt.Errorf("type assertion of `%s` to int failed", key) 35 | } 36 | 37 | // PropertyFloat64 type asserts a property to `float64`. 38 | func (f *Feature) PropertyFloat64(key string) (float64, error) { 39 | if i, ok := (f.Properties[key]).(float64); ok { 40 | return i, nil 41 | } 42 | return 0, fmt.Errorf("type assertion of `%s` to float64 failed", key) 43 | } 44 | 45 | // PropertyString type asserts a property to `string`. 46 | func (f *Feature) PropertyString(key string) (string, error) { 47 | if s, ok := (f.Properties[key]).(string); ok { 48 | return s, nil 49 | } 50 | return "", fmt.Errorf("type assertion of `%s` to string failed", key) 51 | } 52 | 53 | // PropertyMustBool guarantees the return of a `bool` (with optional default) 54 | // 55 | // useful when you explicitly want a `bool` in a single value return context: 56 | // myFunc(f.PropertyMustBool("param1"), f.PropertyMustBool("optional_param", true)) 57 | func (f *Feature) PropertyMustBool(key string, def ...bool) bool { 58 | var defaul bool 59 | 60 | b, err := f.PropertyBool(key) 61 | if err == nil { 62 | return b 63 | } 64 | 65 | if len(def) > 0 { 66 | defaul = def[0] 67 | } 68 | 69 | return defaul 70 | } 71 | 72 | // PropertyMustInt guarantees the return of a `int` (with optional default) 73 | // 74 | // useful when you explicitly want a `int` in a single value return context: 75 | // myFunc(f.PropertyMustInt("param1"), f.PropertyMustInt("optional_param", 123)) 76 | func (f *Feature) PropertyMustInt(key string, def ...int) int { 77 | var defaul int 78 | 79 | b, err := f.PropertyInt(key) 80 | if err == nil { 81 | return b 82 | } 83 | 84 | if len(def) > 0 { 85 | defaul = def[0] 86 | } 87 | 88 | return defaul 89 | } 90 | 91 | // PropertyMustFloat64 guarantees the return of a `float64` (with optional default) 92 | // 93 | // useful when you explicitly want a `float64` in a single value return context: 94 | // myFunc(f.PropertyMustFloat64("param1"), f.PropertyMustFloat64("optional_param", 10.1)) 95 | func (f *Feature) PropertyMustFloat64(key string, def ...float64) float64 { 96 | var defaul float64 97 | 98 | b, err := f.PropertyFloat64(key) 99 | if err == nil { 100 | return b 101 | } 102 | 103 | if len(def) > 0 { 104 | defaul = def[0] 105 | } 106 | 107 | return defaul 108 | } 109 | 110 | // PropertyMustString guarantees the return of a `string` (with optional default) 111 | // 112 | // useful when you explicitly want a `string` in a single value return context: 113 | // myFunc(f.PropertyMustString("param1"), f.PropertyMustString("optional_param", "default")) 114 | func (f *Feature) PropertyMustString(key string, def ...string) string { 115 | var defaul string 116 | 117 | b, err := f.PropertyString(key) 118 | if err == nil { 119 | return b 120 | } 121 | 122 | if len(def) > 0 { 123 | defaul = def[0] 124 | } 125 | 126 | return defaul 127 | } 128 | -------------------------------------------------------------------------------- /properties_test.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func propertiesTestFeature() *Feature { 8 | rawJSON := ` 9 | { "type": "Feature", 10 | "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, 11 | "properties": {"bool":true,"falsebool":false,"int": 1,"float64": 1.2,"string":"text"} 12 | }` 13 | 14 | f, _ := UnmarshalFeature([]byte(rawJSON)) 15 | return f 16 | } 17 | 18 | func TestFeatureSetProperty(t *testing.T) { 19 | f := NewPointFeature([]float64{1, 2}) 20 | f.Properties = nil 21 | f.SetProperty("key", "value") 22 | 23 | if f.PropertyMustString("key") != "value" { 24 | t.Errorf("property not set correctly") 25 | } 26 | } 27 | 28 | func TestFeaturePropertyBool(t *testing.T) { 29 | f := propertiesTestFeature() 30 | 31 | _, err := f.PropertyBool("random") 32 | if err == nil { 33 | t.Errorf("should return error if invalid key") 34 | } 35 | 36 | b, err := f.PropertyBool("bool") 37 | if err != nil { 38 | t.Errorf("should not return error if valid key") 39 | } 40 | 41 | if b != true { 42 | t.Errorf("should return proper property") 43 | } 44 | } 45 | 46 | func TestFeaturePropertyInt(t *testing.T) { 47 | f := propertiesTestFeature() 48 | 49 | _, err := f.PropertyInt("random") 50 | if err == nil { 51 | t.Errorf("should return error if invalid key") 52 | } 53 | 54 | i, err := f.PropertyInt("int") 55 | if err != nil { 56 | t.Errorf("should not return error if valid key") 57 | } 58 | 59 | if i != 1 { 60 | t.Errorf("should return proper property") 61 | } 62 | } 63 | 64 | func TestFeaturePropertyFloat64(t *testing.T) { 65 | f := propertiesTestFeature() 66 | 67 | _, err := f.PropertyFloat64("random") 68 | if err == nil { 69 | t.Errorf("should return error if invalid key") 70 | } 71 | 72 | i, err := f.PropertyFloat64("float64") 73 | if err != nil { 74 | t.Errorf("should not return error if valid key") 75 | } 76 | 77 | if i != 1.2 { 78 | t.Errorf("should return proper property") 79 | } 80 | } 81 | 82 | func TestFeaturePropertyString(t *testing.T) { 83 | f := propertiesTestFeature() 84 | 85 | _, err := f.PropertyString("random") 86 | if err == nil { 87 | t.Errorf("should return error if invalid key") 88 | } 89 | 90 | s, err := f.PropertyString("string") 91 | if err != nil { 92 | t.Errorf("should not return error if valid key") 93 | } 94 | 95 | if s != "text" { 96 | t.Errorf("should return proper property") 97 | } 98 | } 99 | 100 | func TestFeaturePropertyMustBool(t *testing.T) { 101 | f := propertiesTestFeature() 102 | 103 | b := f.PropertyMustBool("random", true) 104 | if b != true { 105 | t.Errorf("should return default if property doesn't exist") 106 | } 107 | 108 | b = f.PropertyMustBool("falsebool", true) 109 | if b != false { 110 | t.Errorf("should return proper property, with default") 111 | } 112 | 113 | b = f.PropertyMustBool("falsebool") 114 | if b != false { 115 | t.Errorf("should return proper property, without default") 116 | } 117 | } 118 | 119 | func TestFeaturePropertyMustInt(t *testing.T) { 120 | f := propertiesTestFeature() 121 | 122 | i := f.PropertyMustInt("random", 10) 123 | if i != 10 { 124 | t.Errorf("should return default if property doesn't exist") 125 | } 126 | 127 | i = f.PropertyMustInt("int", 10) 128 | if i != 1 { 129 | t.Errorf("should return proper property, with default") 130 | } 131 | 132 | i = f.PropertyMustInt("int") 133 | if i != 1 { 134 | t.Errorf("should return proper property, without default") 135 | } 136 | 137 | f.SetProperty("true_int", 5) 138 | i = f.PropertyMustInt("true_int") 139 | if i != 5 { 140 | // json decode makes all things float64, 141 | // but manually setting will be a true int 142 | t.Errorf("should work for true integer types") 143 | } 144 | 145 | i = f.PropertyMustInt("float64") 146 | if i != 1 { 147 | t.Errorf("should convert float64 to int") 148 | } 149 | } 150 | 151 | func TestFeaturePropertyMustFloat64(t *testing.T) { 152 | f := propertiesTestFeature() 153 | 154 | i := f.PropertyMustFloat64("random", 10) 155 | if i != 10 { 156 | t.Errorf("should return default if property doesn't exist") 157 | } 158 | 159 | i = f.PropertyMustFloat64("float64", 10.0) 160 | if i != 1.2 { 161 | t.Errorf("should return proper property, with default") 162 | } 163 | 164 | i = f.PropertyMustFloat64("float64") 165 | if i != 1.2 { 166 | t.Errorf("should return proper property, without default") 167 | } 168 | } 169 | 170 | func TestFeaturePropertyMustString(t *testing.T) { 171 | f := propertiesTestFeature() 172 | 173 | s := f.PropertyMustString("random", "something") 174 | if s != "something" { 175 | t.Errorf("should return default if property doesn't exist") 176 | } 177 | 178 | s = f.PropertyMustString("string", "something") 179 | if s != "text" { 180 | t.Errorf("should return proper property, with default") 181 | } 182 | 183 | s = f.PropertyMustString("string") 184 | if s != "text" { 185 | t.Errorf("should return proper property, without default") 186 | } 187 | } 188 | --------------------------------------------------------------------------------