Skip to content

Commit

Permalink
Merge pull request go-spatial#289 from terranodo/issue-288_geojson_en…
Browse files Browse the repository at this point in the history
…coding

Add GeoJSON encoding to geom package. (go-spatial#288)
  • Loading branch information
gdey authored Feb 23, 2018
2 parents b1ab80b + 197421d commit f741a68
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Tegola is a vector tile server delivering [Mapbox Vector Tiles](https://github.c
## Usage
```
tegola is a vector tile server
Version: v0.5.0
Version: v0.5.0
Usage:
tegola [command]
Expand Down
15 changes: 15 additions & 0 deletions geom/encoding/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package encoding

import (
"fmt"

"github.com/terranodo/tegola/geom"
)

type ErrUnknownGeometry struct {
Geom geom.Geometry
}

func (e ErrUnknownGeometry) Error() string {
return fmt.Sprintf("unknown geometry: %T", e.Geom)
}
119 changes: 119 additions & 0 deletions geom/encoding/geojson/geojson.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package geojson

import (
"encoding/json"

"github.com/terranodo/tegola/geom"
"github.com/terranodo/tegola/geom/encoding"
)

type GeoJSONType string

const (
PointType GeoJSONType = "Point"
MultiPointType GeoJSONType = "MultiPoint"
LineStringType GeoJSONType = "LineString"
MultiLineStringType GeoJSONType = "MultiLineString"
PolygonType GeoJSONType = "Polygon"
MultiPolygonType GeoJSONType = "MultiPolygon"
GeometryCollectionType GeoJSONType = "GeometryCollection"
)

type Geometry struct {
geom.Geometry
}

func (geo Geometry) MarshalJSON() ([]byte, error) {
type coordinates struct {
Type GeoJSONType `json:"type"`
Coords interface{} `json:"coordinates,omitempty"`
}
type collection struct {
Type GeoJSONType `json:"type"`
Geometries []Geometry `json:"geometries,omitempty"`
}

switch g := geo.Geometry.(type) {
case geom.Pointer:
return json.Marshal(coordinates{
Type: PointType,
Coords: g.XY(),
})

case geom.MultiPointer:
return json.Marshal(coordinates{
Type: MultiPointType,
Coords: g.Points(),
})

case geom.LineStringer:
return json.Marshal(coordinates{
Type: LineStringType,
Coords: g.Verticies(),
})

case geom.MultiLineStringer:
return json.Marshal(coordinates{
Type: MultiLineStringType,
Coords: g.LineStrings(),
})

case geom.Polygoner:
return json.Marshal(coordinates{
Type: PolygonType,
Coords: g.LinearRings(),
})

case geom.MultiPolygoner:
return json.Marshal(coordinates{
Type: MultiPolygonType,
Coords: g.Polygons(),
})

case geom.Collectioner:
gs := g.Geometries()

var geos = make([]Geometry, 0, len(gs))
for _, gg := range gs {
geos = append(geos, Geometry{gg})
}

return json.Marshal(collection{
Type: GeometryCollectionType,
Geometries: geos,
})

default:
return nil, encoding.ErrUnknownGeometry{g}
}
}

// featureType allows the GeoJSON type for Feature to be automatically set during json Marshalling
// which avoids the user from accidenlty setting the incorrect GeoJSON type.
type featureType struct{}

func (_ featureType) MarshalJSON() ([]byte, error) {
return []byte(`"Feature"`), nil
}

type Feature struct {
Type featureType `json:"type"`
ID *uint64 `json:"id,omitempty"`
// can be null
Geometry Geometry `json:"geometry"`
// can be null
Properties map[string]interface{} `json:"properties"`
}

// featureCollectionType allows the GeoJSON type for Feature to be automatically set during json Marshalling
// which avoids the user from accidenlty setting the incorrect GeoJSON type.
type featureCollectionType struct{}

func (_ featureCollectionType) MarshalJSON() ([]byte, error) {
return []byte(`"FeatureCollection"`), nil
}

type FeatureCollection struct {
Type featureCollectionType `json:"type"`
Features []Feature `json:"features"`
}
124 changes: 124 additions & 0 deletions geom/encoding/geojson/geojson_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package geojson_test

import (
"encoding/json"
"reflect"
"testing"

"github.com/terranodo/tegola/geom"
"github.com/terranodo/tegola/geom/encoding"
"github.com/terranodo/tegola/geom/encoding/geojson"
)

func TestFeatureMarshalJSON(t *testing.T) {
type tcase struct {
geom geom.Geometry
expected []byte
expectedErr json.MarshalerError
}

fn := func(t *testing.T, tc tcase) {
t.Parallel()

f := geojson.Feature{
Geometry: geojson.Geometry{tc.geom},
}

output, err := json.Marshal(f)
if err != nil && err.Error() != tc.expectedErr.Error() {
t.Errorf("expected err %v got %v", tc.expectedErr.Error(), err)
return
}

if !reflect.DeepEqual(tc.expected, output) {
t.Errorf("expected %v got %v", string(tc.expected), string(output))
return
}
}

tests := map[string]tcase{
"point": {
geom: geom.Point{12.2, 17.7},
expected: []byte(`{"type":"Feature","geometry":{"type":"Point","coordinates":[12.2,17.7]},"properties":null}`),
},
"multi point": {
geom: geom.MultiPoint{{12.2, 17.7}, {13.3, 18.8}},
expected: []byte(`{"type":"Feature","geometry":{"type":"MultiPoint","coordinates":[[12.2,17.7],[13.3,18.8]]},"properties":null}`),
},
"linestring": {
geom: geom.LineString{geom.Point{3.2, 4.3}, geom.Point{5.4, 6.5}, geom.Point{7.6, 8.7}, geom.Point{9.8, 10.9}},
expected: []byte(`{"type":"Feature","geometry":{"type":"LineString","coordinates":[[3.2,4.3],[5.4,6.5],[7.6,8.7],[9.8,10.9]]},"properties":null}`),
},
"multi linestring": {
geom: geom.MultiLineString{
{geom.Point{3.2, 4.3}, geom.Point{5.4, 6.5}, geom.Point{7.6, 8.7}, geom.Point{9.8, 10.9}},
{geom.Point{2.3, 3.4}, geom.Point{4.5, 5.6}, geom.Point{6.7, 7.8}, geom.Point{8.9, 9.10}},
{geom.Point{2.2, 3.3}, geom.Point{4.4, 5.5}, geom.Point{6.6, 7.7}, geom.Point{8.8, 9.9}},
},
expected: []byte(`{"type":"Feature","geometry":{"type":"MultiLineString","coordinates":[[[3.2,4.3],[5.4,6.5],[7.6,8.7],[9.8,10.9]],[[2.3,3.4],[4.5,5.6],[6.7,7.8],[8.9,9.1]],[[2.2,3.3],[4.4,5.5],[6.6,7.7],[8.8,9.9]]]},"properties":null}`),
},
"polygon": {
geom: geom.Polygon{
{
geom.Point{3.2, 4.3}, geom.Point{5.4, 6.5}, geom.Point{7.6, 8.7}, geom.Point{9.8, 10.9}, geom.Point{3.2, 4.3},
},
},
expected: []byte(`{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[3.2,4.3],[5.4,6.5],[7.6,8.7],[9.8,10.9],[3.2,4.3]]]},"properties":null}`),
},
"multi polygon": {
geom: geom.MultiPolygon{
// Polygon 1 w/ holes
geom.Polygon{
// Outer ring
{
geom.Point{10.1, 10.1},
geom.Point{5.5, 20.2},
geom.Point{7.7, 30.3},
geom.Point{30.3, 30.3},
geom.Point{30.3, 10.1},
geom.Point{10.1, 10.1},
},
// Hole 1
{
geom.Point{15.5, 15.5}, geom.Point{11.1, 14.4}, geom.Point{11.1, 11.1},
geom.Point{15.5, 11.1}, geom.Point{15.5, 15.5},
},
// Hole 2
{
geom.Point{25.5, 25.5}, geom.Point{21.1, 24.4}, geom.Point{21.1, 21.1},
geom.Point{25.5, 21.1}, geom.Point{25.5, 25.5},
},
},
// Polygon 2, simple
geom.Polygon{
// Hole 2
{
geom.Point{75.5, 75.5}, geom.Point{71.1, 74.4}, geom.Point{71.1, 71.1},
geom.Point{75.5, 71.1}, geom.Point{75.5, 75.5},
},
},
},
expected: []byte(`{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[10.1,10.1],[5.5,20.2],[7.7,30.3],[30.3,30.3],[30.3,10.1],[10.1,10.1]],[[15.5,15.5],[11.1,14.4],[11.1,11.1],[15.5,11.1],[15.5,15.5]],[[25.5,25.5],[21.1,24.4],[21.1,21.1],[25.5,21.1],[25.5,25.5]]],[[[75.5,75.5],[71.1,74.4],[71.1,71.1],[75.5,71.1],[75.5,75.5]]]]},"properties":null}`),
},
"geometry collection": {
geom: geom.Collection{
geom.Point{12.2, 17.7},
geom.MultiPoint{{12.2, 17.7}, {13.3, 18.8}},
geom.LineString{{3.2, 4.3}, {5.4, 6.5}, {7.6, 8.7}, {9.8, 10.9}},
},
expected: []byte(`{"type":"Feature","geometry":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[12.2,17.7]},{"type":"MultiPoint","coordinates":[[12.2,17.7],[13.3,18.8]]},{"type":"LineString","coordinates":[[3.2,4.3],[5.4,6.5],[7.6,8.7],[9.8,10.9]]}]},"properties":null}`),
},
"nil geom": {
geom: nil,
expectedErr: json.MarshalerError{
Type: reflect.TypeOf(geojson.Geometry{}),
Err: encoding.ErrUnknownGeometry{nil},
},
},
}

for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) { fn(t, tc) })
}
}

0 comments on commit f741a68

Please sign in to comment.