diff --git a/CHANGELOG.md b/CHANGELOG.md index 042d8677..86977dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - Fix a bug affecting only `aarch64` that caused wrong results to be given for line/line intersections. The bug did **not** effect `x64_64`. +- Orient Polygon and MultiPolygon rings as per RFC7946 when serialising as + GeoJSON. + ## v0.42.0 2023-04-02 diff --git a/geom/geojson_marshal_test.go b/geom/geojson_marshal_test.go index 42c9cea9..0d2cfc4a 100644 --- a/geom/geojson_marshal_test.go +++ b/geom/geojson_marshal_test.go @@ -1,7 +1,9 @@ package geom_test import ( + "encoding/hex" "encoding/json" + "strings" "testing" ) @@ -181,13 +183,15 @@ func TestGeoJSONMarshal(t *testing.T) { wkt: `LINESTRING ZM (1 2 3 4,3 4 5 6,5 6 7 8)`, want: `{"type":"LineString","coordinates":[[1,2,3],[3,4,5],[5,6,7]]}`, }, + + // NOTE: Polygons oriented CCW in the output. { wkt: `POLYGON EMPTY`, want: `{"type":"Polygon","coordinates":[]}`, }, { wkt: `POLYGON((0 0,4 0,0 4,0 0),(1 1,2 1,1 2,1 1))`, - want: `{"type":"Polygon","coordinates":[[[0,0],[4,0],[0,4],[0,0]],[[1,1],[2,1],[1,2],[1,1]]]}`, + want: `{"type":"Polygon","coordinates":[[[0,0],[4,0],[0,4],[0,0]],[[1,1],[1,2],[2,1],[1,1]]]}`, }, { wkt: `POLYGON Z EMPTY`, @@ -195,7 +199,7 @@ func TestGeoJSONMarshal(t *testing.T) { }, { wkt: `POLYGON Z ((0 0 0,4 0 1,0 4 1,0 0 1),(1 1 2,2 1 3,1 2 4,1 1 5))`, - want: `{"type":"Polygon","coordinates":[[[0,0,0],[4,0,1],[0,4,1],[0,0,1]],[[1,1,2],[2,1,3],[1,2,4],[1,1,5]]]}`, + want: `{"type":"Polygon","coordinates":[[[0,0,0],[4,0,1],[0,4,1],[0,0,1]],[[1,1,5],[1,2,4],[2,1,3],[1,1,2]]]}`, }, { wkt: `POLYGON M EMPTY`, @@ -203,7 +207,7 @@ func TestGeoJSONMarshal(t *testing.T) { }, { wkt: `POLYGON M ((0 0 0,4 0 1,0 4 1,0 0 1),(1 1 2,2 1 3,1 2 4,1 1 5))`, - want: `{"type":"Polygon","coordinates":[[[0,0],[4,0],[0,4],[0,0]],[[1,1],[2,1],[1,2],[1,1]]]}`, + want: `{"type":"Polygon","coordinates":[[[0,0],[4,0],[0,4],[0,0]],[[1,1],[1,2],[2,1],[1,1]]]}`, }, { wkt: `POLYGON ZM EMPTY`, @@ -211,7 +215,7 @@ func TestGeoJSONMarshal(t *testing.T) { }, { wkt: `POLYGON ZM ((0 0 0 8,4 0 1 3,0 4 1 7,0 0 1 9),(1 1 2 3,2 1 3 7,1 2 4 8,1 1 5 4))`, - want: `{"type":"Polygon","coordinates":[[[0,0,0],[4,0,1],[0,4,1],[0,0,1]],[[1,1,2],[2,1,3],[1,2,4],[1,1,5]]]}`, + want: `{"type":"Polygon","coordinates":[[[0,0,0],[4,0,1],[0,4,1],[0,0,1]],[[1,1,5],[1,2,4],[2,1,3],[1,1,2]]]}`, }, { wkt: `MULTIPOINT EMPTY`, @@ -357,6 +361,8 @@ func TestGeoJSONMarshal(t *testing.T) { wkt: `MULTILINESTRING ZM ((0 1 1 2,2 3 4 8),EMPTY)`, want: `{"type":"MultiLineString","coordinates":[[[0,1,1],[2,3,4]],[]]}`, }, + + // NOTE: MultiPolygons oriented CCW in the output. { wkt: `MULTIPOLYGON EMPTY`, want: `{"type":"MultiPolygon","coordinates":[]}`, @@ -438,14 +444,16 @@ func TestGeoJSONMarshal(t *testing.T) { want: `{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[1,2,3]},{"type":"Point","coordinates":[3,4,5]}]}`, }, } { - t.Run(tt.wkt, func(t *testing.T) { + desc := strings.ReplaceAll(tt.wkt, "(", "_") + desc = strings.ReplaceAll(desc, ")", "_") + t.Run(desc, func(t *testing.T) { geom := geomFromWKT(t, tt.wkt) gotJSON, err := json.Marshal(geom) expectNoErr(t, err) if string(gotJSON) != tt.want { t.Error("json doesn't match") - t.Logf("got: %v", string(gotJSON)) - t.Logf("want: %v", tt.want) + t.Logf("got:\n%v", hex.Dump(gotJSON)) + t.Logf("want:\n%v", hex.Dump([]byte(tt.want))) } }) } diff --git a/geom/type_multi_polygon.go b/geom/type_multi_polygon.go index 669556c7..2e865f30 100644 --- a/geom/type_multi_polygon.go +++ b/geom/type_multi_polygon.go @@ -307,6 +307,7 @@ func (m MultiPolygon) ConvexHull() Geometry { // MarshalJSON implements the encoding/json.Marshaler interface by encoding // this geometry as a GeoJSON geometry object. func (m MultiPolygon) MarshalJSON() ([]byte, error) { + m = m.ForceCCW() var dst []byte dst = append(dst, `{"type":"MultiPolygon","coordinates":`...) dst = appendGeoJSONSequenceMatrix(dst, m.Coordinates()) diff --git a/geom/type_polygon.go b/geom/type_polygon.go index d4fcf8a2..de2712c1 100644 --- a/geom/type_polygon.go +++ b/geom/type_polygon.go @@ -299,6 +299,7 @@ func (p Polygon) ConvexHull() Geometry { // MarshalJSON implements the encoding/json.Marshaler interface by encoding // this geometry as a GeoJSON geometry object. func (p Polygon) MarshalJSON() ([]byte, error) { + p = p.ForceCCW() var dst []byte dst = append(dst, `{"type":"Polygon","coordinates":`...) dst = appendGeoJSONSequences(dst, p.Coordinates())