forked from go-spatial/tegola
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request go-spatial#289 from terranodo/issue-288_geojson_en…
…coding Add GeoJSON encoding to geom package. (go-spatial#288)
- Loading branch information
Showing
4 changed files
with
259 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }) | ||
} | ||
} |