Skip to content

Commit bf6b620

Browse files
committed
Merge branch 'next' into release-0.08
2 parents 5cca8bf + df2cb1e commit bf6b620

File tree

10 files changed

+219
-54
lines changed

10 files changed

+219
-54
lines changed

doc/api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,11 @@ Value Description
310310
`False` Maintain the :class:`~ocgis.RequestDataset`'s longitudinal domain.
311311
================= ====================================================================================================
312312

313+
add_auxiliary_files
314+
~~~~~~~~~~~~~~~~~~~
315+
316+
If ``True``, create a new directory and add metadata and other informational files in addition to the converted file. If ``False``, write the target file only to :attr:`dir_output` and do not create a new directory.
317+
313318
allow_empty
314319
~~~~~~~~~~~
315320

src/ocgis/api/operations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ class OcgOperations(object):
8989
:param interpolate_spatial_bounds: If True and no bounds are available, attempt
9090
to interpolate bounds from centroids.
9191
:type interpolate_spatial_bounds: bool
92-
:param bool add_auxiliary_files: If True, create a new directory and add metadata
93-
and other informational files in addition to the converted file. If False, write
92+
:param bool add_auxiliary_files: If ``True``, create a new directory and add metadata
93+
and other informational files in addition to the converted file. If ``False``, write
9494
the target file only to :attr:`dir_output` and do not create a new directory.
9595
:param function callback: A function taking two parameters: ``percent_complete``
9696
and ``message``.

src/ocgis/api/subset.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def _process_geometries_(self,itr,field,headers,value_keys,alias):
311311
## if there is a slice, use it to subset the field.
312312
elif self.ops.slice is not None:
313313
field = field.__getitem__(self.ops.slice)
314-
314+
315315
## see if the selection crs matches the field's crs
316316
if crs is not None and crs != field.spatial.crs:
317317
geom = project_shapely_geometry(geom,crs.sr,field.spatial.crs.sr)
@@ -435,19 +435,22 @@ def _process_geometries_(self,itr,field,headers,value_keys,alias):
435435
## transform back to rotated pole if necessary
436436
if original_rotated_pole_crs is not None:
437437
if self.ops.output_crs is None and not isinstance(self.ops.output_crs,CFWGS84):
438-
## we need to load the values before proceeding. source
439-
## indices will disappear.
438+
# copy the spatial mask to the new spatial array
439+
spatial_mask_before_transform = deepcopy(sfield.spatial.get_mask())
440+
# need to load the values before proceeding. source indices will disappear.
440441
for variable in sfield.variables.itervalues():
441442
variable.value
442-
## reset the geometries
443+
# reset the geometries
443444
sfield.spatial._geom = None
444445
sfield.spatial.grid = get_rotated_pole_spatial_grid_dimension(
445446
original_rotated_pole_crs,sfield.spatial.grid,inverse=True,
446447
rc_original=original_row_column_metadata)
448+
# update the grid mask with the previous spatial mask
449+
sfield.spatial.grid.value.mask = spatial_mask_before_transform
447450
## update the uid mask to match the spatial mask
448-
sfield.spatial.uid = np.ma.array(sfield.spatial.uid,mask=sfield.spatial.get_mask())
451+
sfield.spatial.uid = np.ma.array(sfield.spatial.uid,mask=spatial_mask_before_transform)
449452
sfield.spatial.crs = original_rotated_pole_crs
450-
453+
451454
## update the coordinate system of the data output
452455
if self.ops.output_crs is not None:
453456
## if the geometry is not None, it may need to be projected to match

src/ocgis/conv/base.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,22 @@ def write(self):
9696
ocgis_lh('starting write method',self._log,logging.DEBUG)
9797

9898
unique_geometry_store = []
99+
100+
# indicates if user geometries should be written to file
101+
write_ugeom = False
99102

100103
try:
101104
build = True
102-
if self._add_ugeom and self.ops is not None and self.ops.geom is not None:
103-
write_geom = True
104-
else:
105-
write_geom = False
105+
106106
for coll in iter(self.colls):
107107
if build:
108+
109+
# write the user geometries if configured and there is one present on the incoming collection.
110+
if self._add_ugeom and coll.geoms.values()[0] is not None:
111+
write_ugeom = True
112+
108113
f = self._build_(coll)
109-
if write_geom:
114+
if write_ugeom:
110115
ugid_shp_name = self.prefix + '_ugid.shp'
111116
ugid_csv_name = self.prefix + '_ugid.csv'
112117

@@ -116,9 +121,14 @@ def write(self):
116121
else:
117122
fiona_path = os.path.join(self.outdir,ugid_shp_name)
118123
csv_path = os.path.join(self.outdir,ugid_csv_name)
119-
124+
120125
if coll.meta is None:
121-
fiona_properties = {'UGID':'int'}
126+
# convert the collection properties to fiona properties
127+
from fiona_ import FionaConverter
128+
fiona_properties = {}
129+
for k, v in coll.properties.values()[0].iteritems():
130+
fiona_properties[k] = FionaConverter.get_field_type(type(v))
131+
122132
fiona_schema = {'geometry':'MultiPolygon',
123133
'properties':fiona_properties}
124134
fiona_meta = {'schema':fiona_schema,'driver':'ESRI Shapefile'}
@@ -148,7 +158,7 @@ def write(self):
148158

149159
build = False
150160
self._write_coll_(f,coll)
151-
if write_geom:
161+
if write_ugeom:
152162
## write the overview geometries to disk
153163
r_geom = coll.geoms.values()[0]
154164
if isinstance(r_geom,Polygon):
@@ -187,7 +197,7 @@ def write(self):
187197
## this the exception we want to log
188198
ocgis_lh(exc=e,logger=self._log)
189199
finally:
190-
if write_geom:
200+
if write_ugeom:
191201
try:
192202
fiona_object.close()
193203
except UnboundLocalError:

src/ocgis/conv/fiona_.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,44 @@ class FionaConverter(AbstractConverter):
3434
np.int16:'int',
3535
np.int32:'int',
3636
str:'str'}
37-
37+
38+
@classmethod
39+
def get_field_type(cls, the_type, key=None, fiona_conversion=None):
40+
"""
41+
:param the_type: The target type object to map to a Fiona field type.
42+
:type the_type: type object
43+
:param key: The key to update the Fiona conversion map.
44+
:type key: str
45+
:param fiona_conversion: A dictionary used to convert Python values to Fiona-expected values.
46+
:type fiona_conversion: dict
47+
"""
48+
49+
ret = None
50+
for k, v in fiona.FIELD_TYPES_MAP.iteritems():
51+
if the_type == v:
52+
ret = k
53+
break
54+
if ret is None:
55+
ret = cls._fiona_type_mapping[the_type]
56+
if the_type in cls._fiona_conversion:
57+
fiona_conversion.update({key.lower(): cls._fiona_conversion[the_type]})
58+
return ret
59+
3860
def _finalize_(self,f):
3961
f['fiona_object'].close()
4062

4163
def _build_(self,coll):
4264
fiona_conversion = {}
4365

44-
def _get_field_type_(key,the_type):
45-
ret = None
46-
for k,v in fiona.FIELD_TYPES_MAP.iteritems():
47-
if the_type == v:
48-
ret = k
49-
break
50-
if ret is None:
51-
ret = self._fiona_type_mapping[the_type]
52-
if the_type in self._fiona_conversion:
53-
fiona_conversion.update({key.lower():self._fiona_conversion[the_type]})
54-
return(ret)
55-
5666
## pull the fiona schema properties together by mapping fiona types to
5767
## the data types of the first row of the output data file
5868
archetype_field = coll._archetype_field
5969
fiona_crs = archetype_field.spatial.crs.value
6070
geom,arch_row = coll.get_iter_dict().next()
6171
fiona_properties = OrderedDict()
6272
for header in coll.headers:
63-
fiona_field_type = _get_field_type_(header,type(arch_row[header]))
73+
fiona_field_type = self.get_field_type(type(arch_row[header]), key=header,
74+
fiona_conversion=fiona_conversion)
6475
fiona_properties.update({header.upper():fiona_field_type})
6576

6677
## we always want to convert the value. if the data is masked, it comes

src/ocgis/test/base.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,28 @@ def _test_bin_dir(self):
3232
ret = os.path.join(base_dir, 'bin')
3333
return (ret)
3434

35-
def assertNumpyAll(self,arr1,arr2):
36-
self.assertEqual(type(arr1),type(arr2))
37-
if isinstance(arr1,np.ma.MaskedArray) or isinstance(arr2,np.ma.MaskedArray):
35+
def assertNumpyAll(self, arr1, arr2):
36+
self.assertEqual(type(arr1), type(arr2))
37+
if isinstance(arr1, np.ma.MaskedArray) or isinstance(arr2, np.ma.MaskedArray):
3838
self.assertTrue(np.all(arr1.data == arr2.data))
3939
self.assertTrue(np.all(arr1.mask == arr2.mask))
40-
self.assertEqual(arr1.fill_value,arr2.fill_value)
41-
return(True)
40+
self.assertEqual(arr1.fill_value, arr2.fill_value)
41+
return True
4242
else:
43-
return(self.assertTrue(np.all(arr1 == arr2)))
43+
return self.assertTrue(np.all(arr1 == arr2))
4444

4545
def assertNumpyAllClose(self, arr1, arr2):
4646
self.assertEqual(type(arr1), type(arr2))
4747
return self.assertTrue(np.allclose(arr1, arr2))
4848

4949
def assertNumpyNotAll(self, arr1, arr2):
50-
self.assertEqual(type(arr1), type(arr2))
51-
return self.assertFalse(np.all(arr1 == arr2))
50+
try:
51+
self.assertNumpyAll(arr1, arr2)
52+
except AssertionError:
53+
ret = True
54+
else:
55+
raise AssertionError('Arrays are equivalent.')
56+
return ret
5257

5358
def assertDictEqual(self, d1, d2, msg=None):
5459
try:
@@ -163,7 +168,10 @@ def get_tdata():
163168
test_data.update(['misc', 'rotated_pole'], 'tas',
164169
'tas_EUR-44_CCCma-CanESM2_rcp85_r1i1p1_SMHI-RCA4_v1_sem_209012-210011.nc',
165170
key='rotated_pole_cccma')
166-
return (test_data)
171+
test_data.update(['misc', 'rotated_pole'], 'pr',
172+
'pr_EUR-11_CNRM-CERFACS-CNRM-CM5_historical_r1i1p1_CLMcom-CCLM4-8-17_v1_mon_198101-199012.nc',
173+
key='rotated_pole_cnrm_cerfacs')
174+
return test_data
167175

168176
def setUp(self):
169177
if self._reset_env: env.reset()

src/ocgis/test/test_ocgis/test_api/test_parms/test_definition.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,33 @@ def test_geom_with_changing_select_ugid(self):
117117
g.select_ugid = [16,17]
118118
self.assertEqual(len(list(g.value)),2)
119119

120+
@staticmethod
121+
def get_geometry_dictionaries():
122+
coordinates = [('France', [2.8, 47.16]),
123+
('Germany', [10.5, 51.29]),
124+
('Italy', [12.2, 43.4])]
125+
geom = []
126+
for ugid, coordinate in enumerate(coordinates, start=1):
127+
point = Point(coordinate[1][0], coordinate[1][1])
128+
geom.append({'geom': point,
129+
'properties': {'UGID': ugid, 'COUNTRY': coordinate[0]}})
130+
return geom
131+
132+
def test_geometry_dictionaries(self):
133+
"""Test geometry dictionaries come out appropriately once formatted."""
134+
135+
geom = self.get_geometry_dictionaries()
136+
#todo: ensure geometry dictionaries have meta associated with them if more than a UGID is present in the properties
137+
g = Geom(geom)
138+
139+
self.assertEqual(len(g.value), 3)
140+
141+
for gdict in g.value:
142+
self.assertEqual(set(gdict.keys()), set(['crs', 'geom', 'properties']))
143+
self.assertIsInstance(gdict['geom'], Point)
144+
self.assertIsInstance(gdict['crs'], CFWGS84)
145+
self.assertEqual(set(gdict['properties'].keys()), set(['UGID', 'COUNTRY']))
146+
120147

121148
class TestTimeRange(TestBase):
122149
_create_dir = False
Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1+
from collections import OrderedDict
2+
from ocgis.conv.numpy_ import NumpyConverter
13
from ocgis.test.base import TestBase
24
import ocgis
35
from ocgis.api.subset import SubsetOperation
46
from ocgis.api.collection import SpatialCollection
57
import itertools
8+
from ocgis.test.test_ocgis.test_api.test_parms.test_definition import TestGeom
69
from ocgis.util.logging_ocgis import ProgressOcgOperations
710

811

9-
class Test(TestBase):
10-
12+
class TestSubsetOperation(TestBase):
13+
1114
def get_operations(self):
1215
rd = self.test_data.get_rd('cancm4_tas')
13-
slc = [None,[0,100],None,[0,10],[0,10]]
14-
ops = ocgis.OcgOperations(dataset=rd,slice=slc)
15-
return(ops)
16-
17-
def test_constructor(self):
18-
for rb,p in itertools.product([True,False],[None,ProgressOcgOperations()]):
19-
sub = SubsetOperation(self.get_operations(),request_base_size_only=rb,
20-
progress=p)
21-
for ii,coll in enumerate(sub):
22-
self.assertIsInstance(coll,SpatialCollection)
23-
self.assertEqual(ii,0)
16+
slc = [None, [0, 100], None, [0, 10], [0, 10]]
17+
ops = ocgis.OcgOperations(dataset=rd, slice=slc)
18+
return ops
19+
20+
def get_subset_operation(self):
21+
geom = TestGeom.get_geometry_dictionaries()
22+
rd = self.test_data.get_rd('cancm4_tas')
23+
ops = ocgis.OcgOperations(dataset=rd, geom=geom, select_nearest=True)
24+
subset = SubsetOperation(ops)
25+
return subset
26+
27+
def test_init(self):
28+
for rb, p in itertools.product([True, False], [None, ProgressOcgOperations()]):
29+
sub = SubsetOperation(self.get_operations(), request_base_size_only=rb, progress=p)
30+
for ii, coll in enumerate(sub):
31+
self.assertIsInstance(coll, SpatialCollection)
32+
self.assertEqual(ii, 0)
33+
34+
def test_geometry_dictionary(self):
35+
"""Test geometry dictionaries come out properly as collections."""
36+
37+
subset = self.get_subset_operation()
38+
conv = NumpyConverter(subset, None, None)
39+
coll = conv.write()
40+
self.assertEqual(coll.properties, OrderedDict([(1, {'COUNTRY': 'France', 'UGID': 1}), (2, {'COUNTRY': 'Germany', 'UGID': 2}), (3, {'COUNTRY': 'Italy', 'UGID': 3})]))
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from collections import OrderedDict
2+
import os
3+
import fiona
4+
import ocgis
5+
from ocgis.api.subset import SubsetOperation
6+
from ocgis.conv.fiona_ import ShpConverter
7+
from ocgis.test.base import TestBase
8+
from ocgis.test.test_ocgis.test_api.test_parms.test_definition import TestGeom
9+
10+
11+
class TestShpConverter(TestBase):
12+
13+
def get_subset_operation(self):
14+
geom = TestGeom.get_geometry_dictionaries()
15+
rd = self.test_data.get_rd('cancm4_tas')
16+
ops = ocgis.OcgOperations(dataset=rd, geom=geom, select_nearest=True, snippet=True)
17+
subset = SubsetOperation(ops)
18+
return subset
19+
20+
def test_attributes_copied(self):
21+
"""Test attributes in geometry dictionaries are properly accounted for in the converter."""
22+
23+
subset = self.get_subset_operation()
24+
conv = ShpConverter(subset, self._test_dir, prefix='shpconv')
25+
ret = conv.write()
26+
27+
path_ugid = os.path.join(self._test_dir, conv.prefix+'_ugid.shp')
28+
29+
with fiona.open(path_ugid) as source:
30+
self.assertEqual(source.schema['properties'], OrderedDict([(u'COUNTRY', 'str'), (u'UGID', 'int:10')]))
31+
32+
def test_none_geom(self):
33+
"""Test a NoneType geometry will pass through the Fiona converter."""
34+
35+
rd = self.test_data.get_rd('cancm4_tas')
36+
slc = [None, 0, None, [10, 20], [10, 20]]
37+
ops = ocgis.OcgOperations(dataset=rd, slice=slc)
38+
subset = SubsetOperation(ops)
39+
conv = ShpConverter(subset, self._test_dir, prefix='shpconv')
40+
ret = conv.write()
41+
contents = os.listdir(self._test_dir)
42+
self.assertEqual(len(contents), 5)

0 commit comments

Comments
 (0)