Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion geojson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ rawJSON, _ := fc.MarshalJSON()
blob, _ := json.Marshal(fc)
```

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

```go
rawJSON := []byte(`
Expand Down
185 changes: 151 additions & 34 deletions geojson/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type Feature struct {
BBox BBox `json:"bbox,omitempty"`
Geometry orb.Geometry `json:"geometry"`
Properties Properties `json:"properties"`

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

// NewFeature creates and initializes a GeoJSON feature given the required attributes.
Expand All @@ -38,31 +44,65 @@ var _ orb.Pointer = &Feature{}
// 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.
// Items in the ExtraMembers map will be included in the base of the
// feature object.
func (f Feature) MarshalJSON() ([]byte, error) {
return marshalJSON(newFeatureDoc(&f))
}

// MarshalBSON 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.
// Items in the ExtraMembers map will be included in the base of the
// feature object.
func (f Feature) MarshalBSON() ([]byte, error) {
return bson.Marshal(newFeatureDoc(&f))
}

func newFeatureDoc(f *Feature) *featureDoc {
doc := &featureDoc{
ID: f.ID,
Type: "Feature",
Properties: f.Properties,
BBox: f.BBox,
Geometry: NewGeometry(f.Geometry),
func newFeatureDoc(f *Feature) interface{} {
if len(f.ExtraMembers) == 0 {
doc := &featureDoc{
ID: f.ID,
Type: "Feature",
Properties: f.Properties,
BBox: f.BBox,
Geometry: NewGeometry(f.Geometry),
}

if len(doc.Properties) == 0 {
doc.Properties = nil
}

return doc
}

if len(doc.Properties) == 0 {
doc.Properties = nil
var tmp map[string]interface{}
if f.ExtraMembers != nil {
tmp = f.ExtraMembers.Clone()
} else {
tmp = make(map[string]interface{}, 3)
}

delete(tmp, "id")
if f.ID != nil {
tmp["id"] = f.ID
}
tmp["type"] = "Feature"

delete(tmp, "bbox")
if f.BBox != nil {
tmp["bbox"] = f.BBox
}

tmp["geometry"] = NewGeometry(f.Geometry)

if len(f.Properties) == 0 {
tmp["properties"] = nil
} else {
tmp["properties"] = f.Properties
}

return doc
return tmp
}

// UnmarshalFeature decodes the data into a GeoJSON feature.
Expand All @@ -85,45 +125,122 @@ func (f *Feature) UnmarshalJSON(data []byte) error {
return nil
}

doc := &featureDoc{}
err := unmarshalJSON(data, &doc)
tmp := make(map[string]nocopyRawMessage, 4)

err := unmarshalJSON(data, &tmp)
if err != nil {
return err
}

return featureUnmarshalFinish(doc, f)
*f = Feature{}
for key, value := range tmp {
switch key {
case "id":
err := unmarshalJSON(value, &f.ID)
if err != nil {
return err
}
case "type":
err := unmarshalJSON(value, &f.Type)
if err != nil {
return err
}
case "bbox":
err := unmarshalJSON(value, &f.BBox)
if err != nil {
return err
}
case "geometry":
g := &Geometry{}
err := unmarshalJSON(value, &g)
if err != nil {
return err
}

if g != nil {
f.Geometry = g.Geometry()
}
case "properties":
err := unmarshalJSON(value, &f.Properties)
if err != nil {
return err
}
default:
if f.ExtraMembers == nil {
f.ExtraMembers = Properties{}
}

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

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

return nil
}

// UnmarshalBSON will unmarshal a BSON document created with bson.Marshal.
func (f *Feature) UnmarshalBSON(data []byte) error {
doc := &featureDoc{}
err := bson.Unmarshal(data, &doc)
tmp := make(map[string]bson.RawValue, 4)

err := bson.Unmarshal(data, &tmp)
if err != nil {
return err
}

return featureUnmarshalFinish(doc, f)
}

func featureUnmarshalFinish(doc *featureDoc, f *Feature) error {
if doc.Type != "Feature" {
return fmt.Errorf("geojson: not a feature: type=%s", doc.Type)
}

var g orb.Geometry
if doc.Geometry != nil {
if doc.Geometry.Coordinates == nil && doc.Geometry.Geometries == nil {
return ErrInvalidGeometry
*f = Feature{}
for key, value := range tmp {
switch key {
case "id":
err := value.Unmarshal(&f.ID)
if err != nil {
return err
}
case "type":
f.Type, _ = bson.RawValue(value).StringValueOK()
case "bbox":
err := value.Unmarshal(&f.BBox)
if err != nil {
return err
}
case "geometry":
g := &Geometry{}
err := value.Unmarshal(&g)
if err != nil {
return err
}

if g != nil {
f.Geometry = g.Geometry()
}
case "properties":
err := value.Unmarshal(&f.Properties)
if err != nil {
return err
}
default:
if f.ExtraMembers == nil {
f.ExtraMembers = Properties{}
}

var val interface{}
err := value.Unmarshal(&val)
if err != nil {
return err
}
f.ExtraMembers[key] = val
}
g = doc.Geometry.Geometry()
}

*f = Feature{
ID: doc.ID,
Type: doc.Type,
Properties: doc.Properties,
BBox: doc.BBox,
Geometry: g,
if f.Type != "Feature" {
return fmt.Errorf("geojson: not a feature: type=%s", f.Type)
}

return nil
Expand Down
55 changes: 55 additions & 0 deletions geojson/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,33 @@ func TestFeatureMarshalJSON_null(t *testing.T) {
})
}

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

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

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

data, err := f.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")
}
}

func TestUnmarshalBSON_missingGeometry(t *testing.T) {
t.Run("missing geometry", func(t *testing.T) {
f := NewFeature(nil)
Expand Down Expand Up @@ -355,6 +382,34 @@ func TestMarshalRing(t *testing.T) {
}
}

func TestFeature_MarshalBSON_extraMembers(t *testing.T) {
f := NewFeature(orb.Point{1, 2})

f.ExtraMembers = map[string]interface{}{
"a": 1.0,
"b": 2.0,
}

data, err := bson.Marshal(f)
if err != nil {
t.Fatalf("unable to marshal feature collection: %v", err)
}

nf := &Feature{}
err = bson.Unmarshal(data, &nf)
if err != nil {
t.Fatalf("unable to unmarshal feature collection: %v", err)
}

if v := nf.ExtraMembers["a"]; v != 1.0 {
t.Errorf("incorrect extra member: %v != %v", v, 1.0)
}

if v := nf.ExtraMembers["b"]; v != 2.0 {
t.Errorf("incorrect extra member: %v != %v", v, 2.0)
}
}

// uncomment to test/benchmark custom json marshalling
// func init() {
// var c = jsoniter.Config{
Expand Down
Loading