Skip to content

Commit 423f6b7

Browse files
committed
Port #1926
Port #1926 with updates for CovJSON Formatting
1 parent 68f5503 commit 423f6b7

2 files changed

Lines changed: 147 additions & 54 deletions

File tree

pygeoapi/formatter/csv_.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import io
3232
import logging
3333

34+
from shapely.geometry import shape as geojson_to_geom
35+
3436
from pygeoapi.formatter.base import BaseFormatter, FormatterSerializationError
3537

3638
LOGGER = logging.getLogger(__name__)
@@ -60,12 +62,28 @@ def write(self, options: dict = {}, data: dict = None) -> str:
6062
Generate data in CSV format
6163
6264
:param options: CSV formatting options
63-
:param data: dict of GeoJSON data
65+
:param data: dict of data
6466
6567
:returns: string representation of format
6668
"""
69+
type = data.get('type') or ''
70+
LOGGER.debug(f'Formatting CSV from data type: {type}')
71+
72+
if 'Feature' in type or 'features' in data:
73+
return self._write_from_geojson(options, data)
74+
75+
def _write_from_geojson(
76+
self, options: dict = {}, data: dict = None, is_point=False
77+
) -> str:
78+
"""
79+
Generate GeoJSON data in CSV format
6780
68-
is_point = False
81+
:param options: CSV formatting options
82+
:param data: dict of GeoJSON data
83+
:param is_point: whether the features are point geometries
84+
85+
:returns: string representation of format
86+
"""
6987
try:
7088
fields = list(data['features'][0]['properties'].keys())
7189
except IndexError:
@@ -75,27 +93,44 @@ def write(self, options: dict = {}, data: dict = None) -> str:
7593
if self.geom:
7694
LOGGER.debug('Including point geometry')
7795
if data['features'][0]['geometry']['type'] == 'Point':
78-
fields.insert(0, 'x')
79-
fields.insert(1, 'y')
96+
LOGGER.debug('point geometry detected, adding x,y columns')
97+
fields.extend(['x', 'y'])
8098
is_point = True
8199
else:
82-
# TODO: implement wkt geometry serialization
83-
LOGGER.debug('not a point geometry, skipping')
100+
LOGGER.debug('not a point geometry, adding wkt column')
101+
fields.append('wkt')
84102

85103
LOGGER.debug(f'CSV fields: {fields}')
104+
output = io.StringIO()
105+
writer = csv.DictWriter(output, fields)
106+
writer.writeheader()
86107

87-
try:
88-
output = io.StringIO()
89-
writer = csv.DictWriter(output, fields)
90-
writer.writeheader()
108+
for feature in data['features']:
109+
self._add_feature(writer, feature, is_point)
91110

92-
for feature in data['features']:
93-
fp = feature['properties']
111+
return output.getvalue().encode('utf-8')
112+
113+
def _add_feature(
114+
self, writer: csv.DictWriter, feature: dict, is_point: bool
115+
) -> None:
116+
"""
117+
Add feature data to CSV writer
118+
119+
:param writer: CSV DictWriter
120+
:param feature: dict of GeoJSON feature
121+
:param is_point: whether the feature is a point geometry
122+
"""
123+
fp = feature['properties']
124+
try:
125+
if self.geom:
94126
if is_point:
95-
fp['x'] = feature['geometry']['coordinates'][0]
96-
fp['y'] = feature['geometry']['coordinates'][1]
97-
LOGGER.debug(fp)
98-
writer.writerow(fp)
127+
[fp['x'], fp['y']] = feature['geometry']['coordinates']
128+
else:
129+
geom = geojson_to_geom(feature['geometry'])
130+
fp['wkt'] = geom.wkt
131+
132+
LOGGER.debug(f'Writing feature to row: {fp}')
133+
writer.writerow(fp)
99134
except ValueError as err:
100135
LOGGER.error(err)
101136
raise FormatterSerializationError('Error writing CSV output')

tests/formatter/test_csv__formatter.py

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,56 +27,114 @@
2727
#
2828
# =================================================================
2929

30-
import csv
31-
import io
30+
from csv import DictReader
31+
from io import StringIO
32+
import json
33+
3234
import pytest
3335

36+
from pygeoapi.formatter.base import FormatterSerializationError
3437
from pygeoapi.formatter.csv_ import CSVFormatter
3538

39+
from ..util import get_test_file_path
3640

37-
@pytest.fixture()
38-
def fixture():
39-
data = {
40-
'features': [{
41-
'geometry': {
42-
'type': 'Point',
43-
'coordinates': [
44-
-130.44472222222223,
45-
54.28611111111111
46-
]
47-
},
48-
'type': 'Feature',
49-
'properties': {
50-
'id': 1972,
51-
'foo': 'bar',
52-
'title': None,
53-
},
54-
'id': 48693
55-
}]
41+
42+
@pytest.fixture
43+
def data():
44+
data_path = get_test_file_path('data/items.geojson')
45+
with open(data_path, 'r', encoding='utf-8') as fh:
46+
return json.load(fh)
47+
48+
49+
@pytest.fixture(scope='function')
50+
def csv_reader_geom_enabled(data):
51+
"""csv_reader with geometry enabled"""
52+
formatter = CSVFormatter({'geom': True})
53+
output = formatter.write(data=data)
54+
return DictReader(StringIO(output.decode('utf-8')))
55+
56+
57+
@pytest.fixture
58+
def invalid_geometry_data():
59+
return {
60+
'features': [
61+
{
62+
'id': 1,
63+
'type': 'Feature',
64+
'properties': {
65+
'id': 1,
66+
'title': 'Invalid Point Feature'
67+
},
68+
'geometry': {
69+
'type': 'Point',
70+
'coordinates': [-130.44472222222223]
71+
}
72+
}
73+
]
5674
}
5775

58-
return data
5976

77+
def test_write_with_geometry_enabled(csv_reader_geom_enabled):
78+
"""Test CSV output with geometry enabled"""
79+
rows = list(csv_reader_geom_enabled)
80+
81+
# Verify the header
82+
header = list(csv_reader_geom_enabled.fieldnames)
83+
assert len(header) == 4
6084

61-
def test_csv__formatter(fixture):
62-
f = CSVFormatter({'geom': True})
63-
f_csv = f.write(data=fixture)
85+
# Verify number of rows
86+
assert len(rows) == 9
6487

65-
buffer = io.StringIO(f_csv.decode('utf-8'))
66-
reader = csv.DictReader(buffer)
6788

68-
header = list(reader.fieldnames)
89+
def test_write_without_geometry(data):
90+
formatter = CSVFormatter({'geom': False})
91+
output = formatter.write(data=data)
92+
csv_reader = DictReader(StringIO(output.decode('utf-8')))
93+
94+
"""Test CSV output with geometry disabled"""
95+
rows = list(csv_reader)
96+
97+
# Verify headers don't include geometry
98+
headers = csv_reader.fieldnames
99+
assert 'geometry' not in headers
100+
101+
# Verify data
102+
first_row = rows[0]
103+
assert first_row['uri'] == \
104+
'http://localhost:5000/collections/objects/items/1'
105+
assert first_row['name'] == 'LineString'
106+
107+
108+
def test_write_empty_features():
109+
"""Test handling of empty feature collection"""
110+
formatter = CSVFormatter({'geom': True})
111+
data = {
112+
'features': []
113+
}
114+
output = formatter.write(data=data)
115+
assert output == ''
116+
69117

70-
assert f.mimetype == 'text/csv; charset=utf-8'
118+
@pytest.mark.parametrize(
119+
'row_index,expected_wkt',
120+
[
121+
(2, 'POINT (-85 33)'),
122+
(3, 'MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))'), # noqa
123+
(4, 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))'),
124+
(5, 'POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))'), # noqa
125+
(6, 'MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))') # noqa
126+
]
127+
)
128+
def test_wkt(csv_reader_geom_enabled, row_index, expected_wkt):
129+
"""Test CSV output of multi-point geometry"""
130+
rows = list(csv_reader_geom_enabled)
71131

72-
assert len(header) == 5
132+
# Verify data
133+
geometry_row = rows[row_index]
134+
assert geometry_row['wkt'] == expected_wkt
73135

74-
assert 'x' in header
75-
assert 'y' in header
76136

77-
data = next(reader)
78-
assert data['x'] == '-130.44472222222223'
79-
assert data['y'] == '54.28611111111111'
80-
assert data['id'] == '1972'
81-
assert data['foo'] == 'bar'
82-
assert data['title'] == ''
137+
def test_invalid_geometry_data(invalid_geometry_data):
138+
formatter = CSVFormatter({'geom': True})
139+
with pytest.raises(FormatterSerializationError):
140+
formatter.write(data=invalid_geometry_data)

0 commit comments

Comments
 (0)