This is an automated email from the git hooks/post-receive script. johanvdw-guest pushed a commit to branch master in repository fiona.
commit 233acfc2810171908a26933e32585aa7ebfc3738 Author: Johan Van de Wauw <jo...@vandewauw.be> Date: Wed Sep 23 22:29:36 2015 +0200 Imported Upstream version 1.6.2 --- CHANGES.txt | 8 ++++++ fiona/__init__.py | 29 +++++++++++-------- fiona/collection.py | 35 +++++++++++++++-------- fiona/ogrext.pyx | 66 ++++++++++++++++++++++++++++++------------- tests/test_bytescollection.py | 2 +- tests/test_collection.py | 2 +- tests/test_collection_crs.py | 22 +++++++++++++++ tests/test_profile.py | 20 +++++++++++++ tests/test_props.py | 43 ++++++++++++++++++++++++++++ 9 files changed, 182 insertions(+), 45 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fbbf1ef..64e8d90 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,14 @@ Changes All issue numbers are relative to https://github.com/Toblerity/Fiona/issues. +1.6.2 (2015-09-22) +------------------ +- Providing only PROJ4 representations in the dataset meta property resulted in + loss of CRS information when using the `fiona.open(..., **src.meta) as dst` + pattern (#265). This bug has been addressed by adding a crs_wkt item to the + meta property and extending the `fiona.open()` and the collection constructor + to look for and prioritize this keyword argument. + 1.6.1 (2015-08-12) ------------------ - Bug fix: Fiona now deserializes JSON-encoded string properties provided by diff --git a/fiona/__init__.py b/fiona/__init__.py index 4c45226..ac0592c 100644 --- a/fiona/__init__.py +++ b/fiona/__init__.py @@ -63,7 +63,7 @@ writing modes) flush contents to disk when their ``with`` blocks end. """ __all__ = ['bounds', 'listlayers', 'open', 'prop_type', 'prop_width'] -__version__ = "1.6.1" +__version__ = "1.6.2" import logging import os @@ -87,15 +87,16 @@ log.addHandler(NullHandler()) def open( - path, - mode='r', - driver=None, - schema=None, + path, + mode='r', + driver=None, + schema=None, crs=None, encoding=None, layer=None, vfs=None, - enabled_drivers=None): + enabled_drivers=None, + crs_wkt=None): """Open file at ``path`` in ``mode`` "r" (read), "a" (append), or "w" (write) and return a ``Collection`` object. @@ -118,7 +119,13 @@ def open( {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True} - + + short hand strings like + + EPSG:4326 + + or WKT representations of coordinate reference systems. + The drivers used by Fiona will try to detect the encoding of data files. If they fail, you may provide the proper ``encoding``, such as 'Windows-1252' for the Natural Earth datasets. @@ -163,10 +170,10 @@ def open( this_schema['properties'] = OrderedDict(schema['properties']) else: this_schema = None - c = Collection(path, mode, - crs=crs, driver=driver, schema=this_schema, + c = Collection(path, mode, + crs=crs, driver=driver, schema=this_schema, encoding=encoding, layer=layer, vsi=vsi, archive=archive, - enabled_drivers=enabled_drivers) + enabled_drivers=enabled_drivers, crs_wkt=crs_wkt) else: raise ValueError( "mode string must be one of 'r', 'w', or 'a', not %s" % mode) @@ -174,6 +181,7 @@ def open( collection = open + def listlayers(path, vfs=None): """Returns a list of layer names in their index order. @@ -257,4 +265,3 @@ def bounds(ob): The ``ob`` may be a feature record or geometry.""" geom = ob.get('geometry') or ob return _bounds(geom) - diff --git a/fiona/collection.py b/fiona/collection.py index e284242..d553582 100644 --- a/fiona/collection.py +++ b/fiona/collection.py @@ -15,8 +15,12 @@ from six import string_types, binary_type class Collection(object): - """A file-like interface to features in the form of GeoJSON-like - mappings.""" + """A file-like interface to features of a vector dataset + + Python text file objects are iterators over lines of a file. Fiona + Collections are similar iterators (not lists!) over features + represented as GeoJSON-like mappings. + """ def __init__( self, path, mode='r', @@ -26,6 +30,7 @@ class Collection(object): vsi=None, archive=None, enabled_drivers=None, + crs_wkt=None, **kwargs): """The required ``path`` is the absolute or relative path to @@ -35,7 +40,9 @@ class Collection(object): a file. In ``mode`` 'w', an OGR ``driver`` name and a ``schema`` are - required. A Proj4 ``crs`` string is recommended. + required. A Proj4 ``crs`` string is recommended. If both ``crs`` + and ``crs_wkt`` keyword arguments are passed, the latter will + trump the former. In 'w' mode, kwargs will be mapped to OGR layer creation options. @@ -51,6 +58,8 @@ class Collection(object): raise TypeError("invalid schema: %r" % schema) if crs and not isinstance(crs, (dict,) + string_types): raise TypeError("invalid crs: %r" % crs) + if crs_wkt and not isinstance(crs_wkt, string_types): + raise TypeError("invalid crs_wkt: %r" % crs_wkt) if encoding and not isinstance(encoding, string_types): raise TypeError("invalid encoding: %r" % encoding) if layer and not isinstance(layer, tuple(list(string_types) + [int])): @@ -119,13 +128,15 @@ class Collection(object): elif 'geometry' not in schema: raise SchemaError("schema lacks: geometry") self._schema = schema - - if crs: + + if crs_wkt: + self._crs_wkt = crs_wkt + elif crs: if 'init' in crs or 'proj' in crs or 'epsg' in crs.lower(): self._crs = crs else: raise CRSError("crs lacks init or proj parameter") - + if driver_count == 0: # create a local manager and enter self.env = GDALEnv() @@ -189,24 +200,24 @@ class Collection(object): @property def crs(self): """Returns a Proj4 string.""" - if self._crs is None and self.mode in ("a", "r") and self.session: + if self._crs is None and self.session: self._crs = self.session.get_crs() return self._crs @property def crs_wkt(self): """Returns a WKT string.""" - if self._crs_wkt is None and self.mode in ("a", "r") and self.session: + if self._crs_wkt is None and self.session: self._crs_wkt = self.session.get_crs_wkt() return self._crs_wkt @property def meta(self): - """Returns a mapping with the driver, schema, and crs properties.""" + """Returns a mapping with the driver, schema, crs, and additional + properties.""" return { - 'driver': self.driver, - 'schema': self.schema, - 'crs': self.crs } + 'driver': self.driver, 'schema': self.schema, 'crs': self.crs, + 'crs_wkt': self.crs_wkt} def filter(self, *args, **kwds): """Returns an iterator over records, but filtered by a test for diff --git a/fiona/ogrext.pyx b/fiona/ogrext.pyx index 4defcb1..85b9b02 100644 --- a/fiona/ogrext.pyx +++ b/fiona/ogrext.pyx @@ -136,7 +136,7 @@ cdef class FeatureBuilder: argument is not destroyed. """ - cdef build(self, void *feature, encoding='utf-8', bbox=False): + cdef build(self, void *feature, encoding='utf-8', bbox=False, driver=None): # The only method anyone ever needs to call cdef void *fdefn cdef int i @@ -186,7 +186,7 @@ cdef class FeatureBuilder: # Does the text contain a JSON object? Let's check. # Let's check as cheaply as we can. - if val.startswith('{'): + if driver == 'GeoJSON' and val.startswith('{'): try: val = json.loads(val) except ValueError as err: @@ -335,7 +335,12 @@ def featureRT(feature, collection): raise ValueError("Null geometry") log.debug("Geometry: %s" % ograpi.OGR_G_ExportToJson(cogr_geometry)) encoding = collection.encoding or 'utf-8' - result = FeatureBuilder().build(cogr_feature, encoding) + result = FeatureBuilder().build( + cogr_feature, + bbox=False, + encoding=encoding, + driver=collection.driver + ) _deleteOgrFeature(cogr_feature) return result @@ -587,14 +592,17 @@ cdef class Session: if self.cogr_layer == NULL: raise ValueError("Null layer") cogr_crs = ograpi.OGR_L_GetSpatialRef(self.cogr_layer) + crs_wkt = "" if cogr_crs is not NULL: log.debug("Got coordinate system") - ograpi.OSRExportToWkt(cogr_crs, &proj_c) - if proj_c == NULL: - raise ValueError("Null projection") - proj_b = proj_c - crs_wkt = proj_b.decode('utf-8') - ograpi.CPLFree(proj_c) + ograpi.OSRExportToWkt(cogr_crs, &proj_c) + if proj_c == NULL: + raise ValueError("Null projection") + proj_b = proj_c + crs_wkt = proj_b.decode('utf-8') + ograpi.CPLFree(proj_c) + else: + log.debug("Projection not found (cogr_crs was NULL)") return crs_wkt def get_extent(self): @@ -652,7 +660,11 @@ cdef class Session: if cogr_feature == NULL: return None feature = FeatureBuilder().build( - cogr_feature, self.get_internalencoding()) + cogr_feature, + bbox=False, + encoding=self.get_internalencoding(), + driver=self.collection.driver + ) _deleteOgrFeature(cogr_feature) return feature @@ -760,19 +772,23 @@ cdef class WritingSession(Session): else: self.cogr_ds = cogr_ds - # Set the spatial reference system from the given crs. - if collection.crs: + # Set the spatial reference system from the crs given to the + # collection constructor. We by-pass the crs_wkt and crs + # properties because they aren't accessible until the layer + # is constructed (later). + col_crs = collection._crs_wkt or collection._crs + if col_crs: cogr_srs = ograpi.OSRNewSpatialReference(NULL) if cogr_srs == NULL: raise ValueError("NULL spatial reference") # First, check for CRS strings like "EPSG:3857". - if isinstance(collection.crs, string_types): - proj_b = collection.crs.encode('utf-8') + if isinstance(col_crs, string_types): + proj_b = col_crs.encode('utf-8') proj_c = proj_b ograpi.OSRSetFromUserInput(cogr_srs, proj_c) - elif isinstance(collection.crs, dict): + elif isinstance(col_crs, dict): # EPSG is a special case. - init = collection.crs.get('init') + init = col_crs.get('init') if init: log.debug("Init: %s", init) auth, val = init.split(':') @@ -781,8 +797,8 @@ cdef class WritingSession(Session): ograpi.OSRImportFromEPSG(cogr_srs, int(val)) else: params = [] - collection.crs['wktext'] = True - for k, v in collection.crs.items(): + col_crs['wktext'] = True + for k, v in col_crs.items(): if v is True or (k in ('no_defs', 'wktext') and v): params.append("+%s" % k) else: @@ -1097,7 +1113,12 @@ cdef class Iterator: if cogr_feature == NULL: raise StopIteration - feature = FeatureBuilder().build(cogr_feature, self.encoding) + feature = FeatureBuilder().build( + cogr_feature, + bbox=False, + encoding=self.encoding, + driver=self.collection.driver + ) _deleteOgrFeature(cogr_feature) return feature @@ -1121,7 +1142,12 @@ cdef class ItemsIterator(Iterator): fid = ograpi.OGR_F_GetFID(cogr_feature) - feature = FeatureBuilder().build(cogr_feature, self.encoding) + feature = FeatureBuilder().build( + cogr_feature, + bbox=False, + encoding=self.encoding, + driver=self.collection.driver + ) _deleteOgrFeature(cogr_feature) return fid, feature diff --git a/tests/test_bytescollection.py b/tests/test_bytescollection.py index e9eaccf..3aecd30 100644 --- a/tests/test_bytescollection.py +++ b/tests/test_bytescollection.py @@ -130,7 +130,7 @@ class ReadingTest(unittest.TestCase): def test_meta(self): self.failUnlessEqual( sorted(self.c.meta.keys()), - ['crs', 'driver', 'schema']) + ['crs', 'crs_wkt', 'driver', 'schema']) def test_bounds(self): self.failUnlessAlmostEqual(self.c.bounds[0], -113.564247, 6) diff --git a/tests/test_collection.py b/tests/test_collection.py index ebd45e0..f49608e 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -185,7 +185,7 @@ class ReadingTest(unittest.TestCase): def test_meta(self): self.failUnlessEqual( sorted(self.c.meta.keys()), - ['crs', 'driver', 'schema']) + ['crs', 'crs_wkt', 'driver', 'schema']) def test_bounds(self): self.failUnlessAlmostEqual(self.c.bounds[0], -113.564247, 6) diff --git a/tests/test_collection_crs.py b/tests/test_collection_crs.py new file mode 100644 index 0000000..ce483ad --- /dev/null +++ b/tests/test_collection_crs.py @@ -0,0 +1,22 @@ +import os +import tempfile + +import fiona + + +def test_collection_crs_wkt(): + with fiona.open('tests/data/coutwildrnp.shp') as src: + assert src.crs_wkt.startswith('GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_84"') + + +def test_collection_no_crs_wkt(): + """crs members of a dataset with no crs can be accessed safely.""" + tmpdir = tempfile.gettempdir() + filename = os.path.join(tmpdir, 'test.shp') + with fiona.open('tests/data/coutwildrnp.shp') as src: + profile = src.meta + del profile['crs'] + del profile['crs_wkt'] + with fiona.open(filename, 'w', **profile) as dst: + assert dst.crs_wkt == "" + assert dst.crs == {} diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..294ca67 --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,20 @@ +import os +import tempfile + +import fiona + + +def test_profile(): + with fiona.open('tests/data/coutwildrnp.shp') as src: + assert src.meta['crs_wkt'] == 'GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295],AUTHORITY["EPSG","4326"]]' + + +def test_profile_creation_wkt(): + tmpdir = tempfile.mkdtemp() + outfilename = os.path.join(tmpdir, 'test.shp') + with fiona.open('tests/data/coutwildrnp.shp') as src: + profile = src.meta + profile['crs'] = 'bogus' + with fiona.open(outfilename, 'w', **profile) as dst: + assert dst.crs == {'init': 'epsg:4326'} + assert dst.crs_wkt == 'GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295],AUTHORITY["EPSG","4326"]]' diff --git a/tests/test_props.py b/tests/test_props.py index 7f36a3b..2f562ac 100644 --- a/tests/test_props.py +++ b/tests/test_props.py @@ -151,3 +151,46 @@ def test_write_json_object_properties(): assert props['upperLeftCoordinate']['latitude'] == 45.66894 assert props['upperLeftCoordinate']['longitude'] == 87.91166 assert props['tricky'] == "{gotcha" + + +def test_json_prop_decode_non_geojson_driver(): + feature = { + "type": "Feature", + "properties": { + "ulc": { + "latitude": 45.66894, + "longitude": 87.91166 + }, + "tricky": "{gotcha" + }, + "geometry": { + "type": "Point", + "coordinates": [10, 15] + } + } + + meta = { + 'crs': 'EPSG:4326', + 'driver': 'ESRI Shapefile', + 'schema': { + 'geometry': 'Point', + 'properties': { + 'ulc': 'str:255', + 'tricky': 'str:255' + } + } + } + + tmpdir = tempfile.mkdtemp() + filename = os.path.join(tmpdir, 'test.json') + with fiona.open(filename, 'w', **meta) as dst: + dst.write(feature) + + with fiona.open(filename) as src: + actual = next(src) + + assert isinstance(actual['properties']['ulc'], text_type) + a = json.loads(actual['properties']['ulc']) + e = json.loads(actual['properties']['ulc']) + assert e == a + assert actual['properties']['tricky'].startswith('{') -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/fiona.git _______________________________________________ Pkg-grass-devel mailing list Pkg-grass-devel@lists.alioth.debian.org http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel