Skip to content

Commit

Permalink
Merge 57587c3 into bc42fc7
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmach committed Jan 7, 2021
2 parents bc42fc7 + 57587c3 commit d086982
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 38 deletions.
36 changes: 30 additions & 6 deletions geojson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ The package also provides helper functions such as `UnmarshalFeatureCollection`
```go
rawJSON := []byte(`
{ "type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
]
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
]
}`)

fc, _ := geojson.UnmarshalFeatureCollection(rawJSON)
Expand All @@ -45,6 +45,30 @@ rawJSON, _ := fc.MarshalJSON()
blob, _ := json.Marshal(fc)
```

#### Foreign/extra members in a feature collection

```go
rawJSON := []byte(`
{ "type": "FeatureCollection",
"generator": "myapp",
"timestamp": "2020-06-15T01:02:03Z",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
]
}`)

fc, _ := geojson.UnmarshalFeatureCollection(rawJSON)

fc.ExtraMembers["generator"] // == "myApp"
fc.ExtraMembers["timestamp"] // == "2020-06-15T01:02:03Z"

// Marshalling will include values in `ExtraMembers` in the
// base featureCollection object.
```

## Feature Properties

GeoJSON features can have properties of any type. This can cause issues in a statically typed
Expand Down
76 changes: 61 additions & 15 deletions geojson/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,67 @@ func ExampleFeature_Point() {

func ExampleFeatureCollection_foreignMembers() {
rawJSON := []byte(`
{ "type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
],
"title": "Title as Foreign Member"
}`)

type MyFeatureCollection struct {
geojson.FeatureCollection
Title string `json:"title"`
{ "type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
],
"title": "Title as Foreign Member"
}`)

fc := geojson.NewFeatureCollection()
json.Unmarshal(rawJSON, &fc)

fmt.Println(fc.Features[0].Geometry)
fmt.Println(fc.ExtraMembers["title"])

data, _ := json.Marshal(fc)
fmt.Println(string(data))

// Output:
// [102 0.5]
// Title as Foreign Member
// {"features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[102,0.5]},"properties":{"prop0":"value0"}}],"title":"Title as Foreign Member","type":"FeatureCollection"}
}

// MyFeatureCollection is a depricated/no longer supported way to extract
// foreign/extra members from a feature collection. Now an UnmarshalJSON
// method, like below, is required for it to work.
type MyFeatureCollection struct {
geojson.FeatureCollection
Title string `json:"title"`
}

// UnmarshalJSON implemented as below is now required for the extra members
// to be decoded directly into the type.
func (fc *MyFeatureCollection) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, &fc.FeatureCollection)
if err != nil {
return err
}

fc.Title = fc.ExtraMembers.MustString("title", "")
return nil
}

func ExampleFeatureCollection_foreignMembersCustom() {
// Note: this approach to handling foreign/extra members requires
// implementing an `UnmarshalJSON` method on the new type.
// See MyFeatureCollection type and its UnmarshalJSON function above.

rawJSON := []byte(`
{ "type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
],
"title": "Title as Foreign Member"
}`)

fc := &MyFeatureCollection{}
json.Unmarshal(rawJSON, &fc)

Expand Down Expand Up @@ -120,7 +166,6 @@ func ExampleFeatureCollection_MarshalJSON() {

// Output:
// {
// "type": "FeatureCollection",
// "features": [
// {
// "type": "Feature",
Expand All @@ -133,6 +178,7 @@ func ExampleFeatureCollection_MarshalJSON() {
// },
// "properties": null
// }
// ]
// ],
// "type": "FeatureCollection"
// }
}
93 changes: 77 additions & 16 deletions geojson/feature_collection.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/*
Package geojson is a library for encoding and decoding GeoJSON into Go structs using
the geometries in the orb package. Supports both the json.Marshaler and json.Unmarshaler
interfaces as well as helper functions such as `UnmarshalFeatureCollection` and `UnmarshalFeature`.
Package geojson is a library for encoding and decoding GeoJSON into Go structs
using the geometries in the orb package. Supports both the json.Marshaler and
json.Unmarshaler interfaces as well as helper functions such as
`UnmarshalFeatureCollection` and `UnmarshalFeature`.
*/
package geojson

Expand All @@ -17,6 +18,11 @@ type FeatureCollection struct {
Type string `json:"type"`
BBox BBox `json:"bbox,omitempty"`
Features []*Feature `json:"features"`

// ExtraMembers can be used to encoded/decode extra key/members in
// the base of the feature collection. Note that keys of "type", "bbox"
// and "features" will not work as those are reserved by the GeoJSON spec.
ExtraMembers Properties `json:"-"`
}

// NewFeatureCollection creates and initializes a new feature collection.
Expand All @@ -36,33 +42,88 @@ func (fc *FeatureCollection) Append(feature *Feature) *FeatureCollection {
// 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.
// Items in the ExtraMembers map will be included in the base of the
// feature collection object.
func (fc FeatureCollection) MarshalJSON() ([]byte, error) {
type tempFC FeatureCollection
var tmp map[string]interface{}
if fc.ExtraMembers != nil {
tmp = fc.ExtraMembers.Clone()
} else {
tmp = make(map[string]interface{}, 3)
}

c := tempFC{
Type: featureCollection,
BBox: fc.BBox,
Features: fc.Features,
tmp["type"] = featureCollection
delete(tmp, "bbox")
if fc.BBox != nil {
tmp["bbox"] = fc.BBox
}
if fc.Features == nil {
tmp["features"] = []*Feature{}
} else {
tmp["features"] = fc.Features
}

return json.Marshal(tmp)
}

// UnmarshalJSON decodes the data into a GeoJSON feature collection.
// Extra/foreign members will be put into the `ExtraMembers` attribute.
func (fc *FeatureCollection) UnmarshalJSON(data []byte) error {
tmp := make(map[string]json.RawMessage, 4)

if c.Features == nil {
c.Features = []*Feature{}
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
return json.Marshal(c)

*fc = FeatureCollection{}
for key, value := range tmp {
switch key {
case "type":
err := json.Unmarshal(value, &fc.Type)
if err != nil {
return err
}
case "bbox":
err := json.Unmarshal(value, &fc.BBox)
if err != nil {
return err
}
case "features":
err := json.Unmarshal(value, &fc.Features)
if err != nil {
return err
}
default:
if fc.ExtraMembers == nil {
fc.ExtraMembers = Properties{}
}

var val interface{}
err := json.Unmarshal(value, &val)
if err != nil {
return err
}
fc.ExtraMembers[key] = val
}
}

if fc.Type != featureCollection {
return fmt.Errorf("geojson: not a feature collection: type=%s", fc.Type)
}

return nil
}

// 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)

err := fc.UnmarshalJSON(data)
if err != nil {
return nil, err
}

if fc.Type != featureCollection {
return nil, fmt.Errorf("geojson: not a feature collection: type=%s", fc.Type)
}

return fc, nil
}
31 changes: 31 additions & 0 deletions geojson/feature_collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,34 @@ func TestFeatureCollectionMarshalValue(t *testing.T) {
t.Errorf("json should set features object to at least empty array")
}
}

func TestFeatureCollectionMarshalJSON_extraMembers(t *testing.T) {
rawJSON := `
{ "type": "FeatureCollection",
"foo": "bar",
"features": [
{ "type": "Feature",
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
}
]
}`

fc, err := UnmarshalFeatureCollection([]byte(rawJSON))
if err != nil {
t.Fatalf("should unmarshal feature collection without issue, err %v", err)
}

if v := fc.ExtraMembers.MustString("foo", ""); v != "bar" {
t.Errorf("missing extra: foo: %v", v)
}

data, err := fc.MarshalJSON()
if err != nil {
t.Fatalf("unable to marshal: %v", err)
}

if !bytes.Contains(data, []byte(`"foo":"bar"`)) {
t.Fatalf("extras not in marshalled data")
}
}
40 changes: 40 additions & 0 deletions geojson/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package geojson
import (
"bytes"
"encoding/json"
"io/ioutil"
"strings"
"testing"

Expand Down Expand Up @@ -275,3 +276,42 @@ func TestMarshalRing(t *testing.T) {
t.Logf("%v", string(data))
}
}

func BenchmarkFeatureMarshalJSON(b *testing.B) {
data, err := ioutil.ReadFile("../encoding/mvt/testdata/16-17896-24449.json")
if err != nil {
b.Fatalf("could not open file: %v", err)
}

tile := map[string]*FeatureCollection{}
err = json.Unmarshal(data, &tile)
if err != nil {
b.Fatalf("could not unmarshal: %v", err)
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(tile)
if err != nil {
b.Fatalf("marshal error: %v", err)
}
}
}

func BenchmarkFeatureUnmarshalJSON(b *testing.B) {
data, err := ioutil.ReadFile("../encoding/mvt/testdata/16-17896-24449.json")
if err != nil {
b.Fatalf("could not open file: %v", err)
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tile := map[string]*FeatureCollection{}
err = json.Unmarshal(data, &tile)
if err != nil {
b.Fatalf("could not unmarshal: %v", err)
}
}
}
2 changes: 1 addition & 1 deletion geojson/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (p Properties) MustString(key string, def ...string) string {

// Clone returns a shallow copy of the properties.
func (p Properties) Clone() Properties {
n := make(Properties, len(p))
n := make(Properties, len(p)+3)
for k, v := range p {
n[k] = v
}
Expand Down

0 comments on commit d086982

Please sign in to comment.