Skip to content

Commit 39acdd9

Browse files
sofiapagjohnk1
andauthored
add validation for linear ring being closed (#167)
* updated the validation to use geojson library as backup for existing json validation * 1.8.0 Co-authored-by: John Klancer <[email protected]>
1 parent 9478b3a commit 39acdd9

File tree

8 files changed

+121
-11
lines changed

8 files changed

+121
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
=======
44

5+
# 1.8.0 (2022-08-25)
6+
- validates source in `upload-source` command using geojson library.
7+
- Provides line number for an invalid feature that is detected with the geojson validator.
8+
59
# 1.7.4 (2022-07-13)
610
- validates source id for correct syntax when `upload-source` command to resolve `Connection reset by peer error`
711

mapbox_tilesets/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""mapbox_tilesets package"""
22

3-
__version__ = "1.7.4"
3+
__version__ = "1.8.0"

mapbox_tilesets/scripts/cli.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,8 @@ def validate_source(features):
504504
"""
505505
click.echo("Validating features", err=True)
506506

507-
for feature in features:
508-
utils.validate_geojson(feature)
507+
for index, feature in enumerate(features):
508+
utils.validate_geojson(index, feature)
509509

510510
click.echo("✔ valid")
511511

@@ -582,9 +582,9 @@ def _upload_source(
582582
)
583583

584584
with tempfile.TemporaryFile() as file:
585-
for feature in features:
585+
for index, feature in enumerate(features):
586586
if not no_validation:
587-
utils.validate_geojson(feature)
587+
utils.validate_geojson(index, feature)
588588

589589
file.write(
590590
(json.dumps(feature, separators=(",", ":")) + "\n").encode("utf-8")
@@ -730,8 +730,8 @@ def list_sources(username, token=None):
730730

731731

732732
def validate_stream(features):
733-
for feature in features:
734-
utils.validate_geojson(feature)
733+
for index, feature in enumerate(features):
734+
utils.validate_geojson(index, feature)
735735
yield feature
736736

737737

mapbox_tilesets/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from requests import Session
99

1010
import mapbox_tilesets
11+
import geojson
12+
import json
1113

1214

1315
def load_module(modulename):
@@ -71,7 +73,15 @@ def validate_tileset_id(tileset_id):
7173
return re.match(pattern, tileset_id, flags=re.IGNORECASE)
7274

7375

74-
def validate_geojson(feature):
76+
def geojson_validate(index, feature):
77+
geojsonFeature = geojson.loads(json.dumps(feature))
78+
if not geojsonFeature.is_valid:
79+
raise mapbox_tilesets.errors.TilesetsError(
80+
f"Error in feature number {index}: " + "".join(geojsonFeature.errors())
81+
)
82+
83+
84+
def validate_geojson(index, feature):
7585
schema = {
7686
"definitions": {},
7787
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -116,8 +126,8 @@ def validate_geojson(feature):
116126
},
117127
},
118128
}
119-
120-
return validate(instance=feature, schema=schema)
129+
validate(instance=feature, schema=schema)
130+
geojson_validate(index, feature)
121131

122132

123133
def _convert_precision_to_zoom(precision):

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ requests-toolbelt==0.9.1
77
jsonschema==3.0.1
88
jsonseq==1.0.0
99
mercantile==1.1.6
10-
supermercado==0.2.0
10+
supermercado==0.2.0
11+
geojson===2.5.0
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"type": "Feature",
3+
"id":"01",
4+
"geometry": {
5+
"type": "Polygon",
6+
"coordinates": [
7+
[
8+
[-150.957,-40.5948],
9+
[-155,-41],
10+
[-152,-42],
11+
[-152, -40]
12+
]
13+
]
14+
},
15+
"properties":{"name":"Ducky Loo"}
16+
}

tests/test_cli_sources.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,41 @@ def side_effect(fields):
224224
)
225225

226226

227+
@pytest.mark.usefixtures("token_environ")
228+
@mock.patch("mapbox_tilesets.scripts.cli.MultipartEncoder")
229+
@mock.patch("mapbox_tilesets.scripts.cli.MultipartEncoderMonitor")
230+
@mock.patch("requests.Session.post")
231+
def test_cli_upload_source_invalid_polygon(
232+
mock_request_post,
233+
mock_multipart_encoder,
234+
MockResponse,
235+
MockMultipartEncoding,
236+
):
237+
expected_json = b'{"type":"Feature","id":"01","geometry":{"type":"Polygon","coordinates":[[-150.957,-40.5948],[-155,-41],[-152,-42],[-152,-40]]},"properties":{"name":"Ducky Loo"}}\n'
238+
239+
def side_effect(fields):
240+
assert fields["file"][1].read() == expected_json
241+
return MockMultipartEncoding()
242+
243+
mock_multipart_encoder.side_effect = side_effect
244+
245+
runner = CliRunner()
246+
validated_result = runner.invoke(
247+
upload_source,
248+
[
249+
"test-user",
250+
"populated-places-source",
251+
"tests/fixtures/invalid-polygon.ldgeojson",
252+
],
253+
)
254+
assert validated_result.exit_code == 1
255+
256+
assert (
257+
str(validated_result.exception)
258+
== "Error in feature number 0: Each linear ring must end where it started"
259+
)
260+
261+
227262
@pytest.mark.usefixtures("token_environ")
228263
def validate_source_id(self):
229264
self.assertRaises(

tests/test_utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
_get_api,
66
_get_session,
77
_get_token,
8+
geojson_validate,
89
validate_tileset_id,
910
_convert_precision_to_zoom,
1011
calculate_tiles_area,
@@ -70,6 +71,49 @@ def test_validate_tileset_id_toolong():
7071
assert not validate_tileset_id(tileset)
7172

7273

74+
def test_geojson_validate():
75+
geometry = {"type": "Polygon", "coordinates": [[[1, 2], [3, 4], [5, 6]]]}
76+
with pytest.raises(TilesetsError) as excinfo:
77+
geojson_validate(2, geometry)
78+
79+
assert (
80+
str(excinfo.value)
81+
== "Error in feature number 2: Each linear ring must contain at least 4 positions"
82+
)
83+
84+
85+
def test_geojson_validate_open_ring():
86+
geometry = {"type": "Polygon", "coordinates": [[[1, 2], [3, 4], [5, 6], [7, 8]]]}
87+
with pytest.raises(TilesetsError) as excinfo:
88+
geojson_validate(2, geometry)
89+
90+
assert (
91+
str(excinfo.value)
92+
== "Error in feature number 2: Each linear ring must end where it started"
93+
)
94+
95+
96+
def test_geojson_validate_closed_ring():
97+
geometry = {"type": "Polygon", "coordinates": [[[1, 2], [3, 4], [5, 6], [1, 2]]]}
98+
assert geojson_validate(2, geometry) is None
99+
100+
101+
def test_geojson_validate_closed_ring_correct_winding_order():
102+
geometry = {
103+
"type": "Polygon",
104+
"coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
105+
}
106+
assert geojson_validate(2, geometry) is None
107+
108+
109+
def test_geojson_validate_closed_ring_incorrect_winding_order():
110+
geometry = {
111+
"type": "Polygon",
112+
"coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
113+
}
114+
assert geojson_validate(2, geometry) is None
115+
116+
73117
def test_convert_precision_to_zoom_10m():
74118
precision = "10m"
75119
assert _convert_precision_to_zoom(precision) == 6

0 commit comments

Comments
 (0)