Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

geojson: add support for "external" json encoders/decoders #98

Merged
merged 2 commits into from
Apr 20, 2022
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ for _, f := range fc {
}
```

The library supports third party "encoding/json" replacements
such [github.com/json-iterator/go](https://github.com/json-iterator/go).
See the [geojson](geojson) readme for more details.

## Mapbox Vector Tiles

The [encoding/mvt](encoding/mvt) sub-package implements Marshalling and
Expand Down
46 changes: 46 additions & 0 deletions geojson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ fc.ExtraMembers["timestamp"] // == "2020-06-15T01:02:03Z"
// base featureCollection object.
```

## Performance

For performance critical applications, consider a
third party replacement of "encoding/json" like [github.com/json-iterator/go](https://github.com/json-iterator/go)

This can be enabled with something like this:

```go
import (
jsoniter "github.com/json-iterator/go"
"github.com/paulmach/orb"
)

var c = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: false,
MarshalFloatWith6Digits: true,
}.Froze()

CustomJSONMarshaler = c
CustomJSONUnmarshaler = c
```

The above change can have dramatic performance implications, see the benchmarks below
on a 100k feature collection file:

```
benchmark old ns/op new ns/op delta
BenchmarkFeatureMarshalJSON-12 2694543 733480 -72.78%
BenchmarkFeatureUnmarshalJSON-12 5383825 2738183 -49.14%
BenchmarkGeometryMarshalJSON-12 210107 62789 -70.12%
BenchmarkGeometryUnmarshalJSON-12 691472 144689 -79.08%

benchmark old allocs new allocs delta
BenchmarkFeatureMarshalJSON-12 7818 2316 -70.38%
BenchmarkFeatureUnmarshalJSON-12 23047 31946 +38.61%
BenchmarkGeometryMarshalJSON-12 2 3 +50.00%
BenchmarkGeometryUnmarshalJSON-12 2042 18 -99.12%

benchmark old bytes new bytes delta
BenchmarkFeatureMarshalJSON-12 794088 490251 -38.26%
BenchmarkFeatureUnmarshalJSON-12 766354 1068497 +39.43%
BenchmarkGeometryMarshalJSON-12 24787 18650 -24.76%
BenchmarkGeometryUnmarshalJSON-12 79784 51374 -35.61%
```

## Feature Properties

GeoJSON features can have properties of any type. This can cause issues in a statically typed
Expand Down
5 changes: 2 additions & 3 deletions geojson/feature.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package geojson

import (
"encoding/json"
"fmt"

"github.com/paulmach/orb"
Expand Down Expand Up @@ -50,7 +49,7 @@ func (f Feature) MarshalJSON() ([]byte, error) {
jf.Properties = nil
}

return json.Marshal(jf)
return marshalJSON(jf)
}

// UnmarshalFeature decodes the data into a GeoJSON feature.
Expand All @@ -69,7 +68,7 @@ func UnmarshalFeature(data []byte) (*Feature, error) {
// into the orb.Geometry types.
func (f *Feature) UnmarshalJSON(data []byte) error {
jf := &jsonFeature{}
err := json.Unmarshal(data, &jf)
err := unmarshalJSON(data, &jf)
if err != nil {
return err
}
Expand Down
13 changes: 6 additions & 7 deletions geojson/feature_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ json.Unmarshaler interfaces as well as helper functions such as
package geojson

import (
"encoding/json"
"fmt"
)

Expand Down Expand Up @@ -63,15 +62,15 @@ func (fc FeatureCollection) MarshalJSON() ([]byte, error) {
tmp["features"] = fc.Features
}

return json.Marshal(tmp)
return marshalJSON(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]nocopyRawMessage, 4)

err := json.Unmarshal(data, &tmp)
err := unmarshalJSON(data, &tmp)
if err != nil {
return err
}
Expand All @@ -80,17 +79,17 @@ func (fc *FeatureCollection) UnmarshalJSON(data []byte) error {
for key, value := range tmp {
switch key {
case "type":
err := json.Unmarshal(value, &fc.Type)
err := unmarshalJSON(value, &fc.Type)
if err != nil {
return err
}
case "bbox":
err := json.Unmarshal(value, &fc.BBox)
err := unmarshalJSON(value, &fc.BBox)
if err != nil {
return err
}
case "features":
err := json.Unmarshal(value, &fc.Features)
err := unmarshalJSON(value, &fc.Features)
if err != nil {
return err
}
Expand All @@ -100,7 +99,7 @@ func (fc *FeatureCollection) UnmarshalJSON(data []byte) error {
}

var val interface{}
err := json.Unmarshal(value, &val)
err := unmarshalJSON(value, &val)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions geojson/feature_collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestUnmarshalFeatureCollection(t *testing.T) {

// check unmarshal/marshal loop
var expected interface{}
err = json.Unmarshal([]byte(rawJSON), &expected)
err = unmarshalJSON([]byte(rawJSON), &expected)
if err != nil {
t.Fatalf("unmarshal error: %v", err)
}
Expand All @@ -95,7 +95,7 @@ func TestUnmarshalFeatureCollection(t *testing.T) {
}

var raw interface{}
err = json.Unmarshal(data, &raw)
err = unmarshalJSON(data, &raw)
if err != nil {
t.Fatalf("unmarshal error: %v", err)
}
Expand Down
17 changes: 15 additions & 2 deletions geojson/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,19 @@ func TestMarshalRing(t *testing.T) {
}
}

// uncomment to test/benchmark custom json marshalling
// func init() {
// var c = jsoniter.Config{
// EscapeHTML: true,
// SortMapKeys: false,
// ValidateJsonRawMessage: false,
// MarshalFloatWith6Digits: true,
// }.Froze()

// CustomJSONMarshaler = c
// CustomJSONUnmarshaler = c
// }

func BenchmarkFeatureMarshalJSON(b *testing.B) {
data, err := ioutil.ReadFile("../encoding/mvt/testdata/16-17896-24449.json")
if err != nil {
Expand All @@ -296,7 +309,7 @@ func BenchmarkFeatureMarshalJSON(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(tile)
_, err := marshalJSON(tile)
if err != nil {
b.Fatalf("marshal error: %v", err)
}
Expand All @@ -313,7 +326,7 @@ func BenchmarkFeatureUnmarshalJSON(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
tile := map[string]*FeatureCollection{}
err = json.Unmarshal(data, &tile)
err = unmarshalJSON(data, &tile)
if err != nil {
b.Fatalf("could not unmarshal: %v", err)
}
Expand Down
52 changes: 23 additions & 29 deletions geojson/geometry.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package geojson

import (
"encoding/json"
"errors"

"github.com/paulmach/orb"
)

Expand Down Expand Up @@ -85,14 +85,15 @@ func (g Geometry) MarshalJSON() ([]byte, error) {
ng.Geometries = g.Geometries
ng.Type = orb.Collection{}.GeoJSONType()
}
return json.Marshal(ng)

return marshalJSON(ng)
}

// UnmarshalGeometry decodes the data into a GeoJSON feature.
// 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)
err := unmarshalJSON(data, g)
if err != nil {
return nil, err
}
Expand All @@ -103,35 +104,35 @@ func UnmarshalGeometry(data []byte) (*Geometry, error) {
// UnmarshalJSON will unmarshal the correct geometry from the json structure.
func (g *Geometry) UnmarshalJSON(data []byte) error {
jg := &jsonGeometry{}
err := json.Unmarshal(data, jg)
err := unmarshalJSON(data, jg)
if err != nil {
return err
}

switch jg.Type {
case "Point":
p := orb.Point{}
err = json.Unmarshal(jg.Coordinates, &p)
err = unmarshalJSON(jg.Coordinates, &p)
g.Coordinates = p
case "MultiPoint":
mp := orb.MultiPoint{}
err = json.Unmarshal(jg.Coordinates, &mp)
err = unmarshalJSON(jg.Coordinates, &mp)
g.Coordinates = mp
case "LineString":
ls := orb.LineString{}
err = json.Unmarshal(jg.Coordinates, &ls)
err = unmarshalJSON(jg.Coordinates, &ls)
g.Coordinates = ls
case "MultiLineString":
mls := orb.MultiLineString{}
err = json.Unmarshal(jg.Coordinates, &mls)
err = unmarshalJSON(jg.Coordinates, &mls)
g.Coordinates = mls
case "Polygon":
p := orb.Polygon{}
err = json.Unmarshal(jg.Coordinates, &p)
err = unmarshalJSON(jg.Coordinates, &p)
g.Coordinates = p
case "MultiPolygon":
mp := orb.MultiPolygon{}
err = json.Unmarshal(jg.Coordinates, &mp)
err = unmarshalJSON(jg.Coordinates, &mp)
g.Coordinates = mp
case "GeometryCollection":
g.Geometries = jg.Geometries
Expand All @@ -154,13 +155,13 @@ func (p Point) Geometry() orb.Geometry {

// MarshalJSON will convert the Point into a GeoJSON Point geometry.
func (p Point) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.Point(p)})
return marshalJSON(Geometry{Coordinates: orb.Point(p)})
}

// UnmarshalJSON will unmarshal the GeoJSON Point geometry.
func (p *Point) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -184,13 +185,13 @@ func (mp MultiPoint) Geometry() orb.Geometry {

// MarshalJSON will convert the MultiPoint into a GeoJSON MultiPoint geometry.
func (mp MultiPoint) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.MultiPoint(mp)})
return marshalJSON(Geometry{Coordinates: orb.MultiPoint(mp)})
}

// UnmarshalJSON will unmarshal the GeoJSON MultiPoint geometry.
func (mp *MultiPoint) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -214,13 +215,13 @@ func (ls LineString) Geometry() orb.Geometry {

// MarshalJSON will convert the LineString into a GeoJSON LineString geometry.
func (ls LineString) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.LineString(ls)})
return marshalJSON(Geometry{Coordinates: orb.LineString(ls)})
}

// UnmarshalJSON will unmarshal the GeoJSON MultiPoint geometry.
func (ls *LineString) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -244,13 +245,13 @@ func (mls MultiLineString) Geometry() orb.Geometry {

// MarshalJSON will convert the MultiLineString into a GeoJSON MultiLineString geometry.
func (mls MultiLineString) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.MultiLineString(mls)})
return marshalJSON(Geometry{Coordinates: orb.MultiLineString(mls)})
}

// UnmarshalJSON will unmarshal the GeoJSON MultiPoint geometry.
func (mls *MultiLineString) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -274,13 +275,13 @@ func (p Polygon) Geometry() orb.Geometry {

// MarshalJSON will convert the Polygon into a GeoJSON Polygon geometry.
func (p Polygon) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.Polygon(p)})
return marshalJSON(Geometry{Coordinates: orb.Polygon(p)})
}

// UnmarshalJSON will unmarshal the GeoJSON Polygon geometry.
func (p *Polygon) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -304,13 +305,13 @@ func (mp MultiPolygon) Geometry() orb.Geometry {

// MarshalJSON will convert the MultiPolygon into a GeoJSON MultiPolygon geometry.
func (mp MultiPolygon) MarshalJSON() ([]byte, error) {
return json.Marshal(Geometry{Coordinates: orb.MultiPolygon(mp)})
return marshalJSON(Geometry{Coordinates: orb.MultiPolygon(mp)})
}

// UnmarshalJSON will unmarshal the GeoJSON MultiPolygon geometry.
func (mp *MultiPolygon) UnmarshalJSON(data []byte) error {
g := &Geometry{}
err := json.Unmarshal(data, &g)
err := unmarshalJSON(data, &g)
if err != nil {
return err
}
Expand All @@ -335,10 +336,3 @@ type jsonGeometryMarshall struct {
Coordinates orb.Geometry `json:"coordinates,omitempty"`
Geometries []*Geometry `json:"geometries,omitempty"`
}

type nocopyRawMessage []byte

func (m *nocopyRawMessage) UnmarshalJSON(data []byte) error {
*m = data
return nil
}
Loading