From 825d6edd69404de29c6fb8be568176f03b0c982c Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Tue, 22 Jul 2008 01:10:59 +0000 Subject: [PATCH] gis: gdal: Features may now be fetched from OGR layers that do not support random access reading, but no more negative indexes are allowed; cleaned up `OGRGeomType`; moved test vector data into its own directory. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@8034 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/gdal/error.py | 1 + django/contrib/gis/gdal/geometries.py | 8 +- django/contrib/gis/gdal/geomtype.py | 67 ++++++------ django/contrib/gis/gdal/layer.py | 61 +++++++---- django/contrib/gis/gdal/prototypes/ds.py | 1 + .../{ => data}/test_point/test_point.dbf | Bin .../{ => data}/test_point/test_point.prj | 0 .../{ => data}/test_point/test_point.shp | Bin .../{ => data}/test_point/test_point.shx | Bin .../tests/{ => data}/test_poly/test_poly.dbf | Bin .../tests/{ => data}/test_poly/test_poly.prj | 0 .../tests/{ => data}/test_poly/test_poly.shp | Bin .../tests/{ => data}/test_poly/test_poly.shx | Bin .../gis/tests/data/test_vrt/test_vrt.csv | 4 + .../gis/tests/data/test_vrt/test_vrt.vrt | 7 ++ django/contrib/gis/tests/test_gdal_ds.py | 95 +++++++++++++----- django/contrib/gis/tests/test_gdal_geom.py | 5 + 17 files changed, 174 insertions(+), 75 deletions(-) rename django/contrib/gis/tests/{ => data}/test_point/test_point.dbf (100%) rename django/contrib/gis/tests/{ => data}/test_point/test_point.prj (100%) rename django/contrib/gis/tests/{ => data}/test_point/test_point.shp (100%) rename django/contrib/gis/tests/{ => data}/test_point/test_point.shx (100%) rename django/contrib/gis/tests/{ => data}/test_poly/test_poly.dbf (100%) rename django/contrib/gis/tests/{ => data}/test_poly/test_poly.prj (100%) rename django/contrib/gis/tests/{ => data}/test_poly/test_poly.shp (100%) rename django/contrib/gis/tests/{ => data}/test_poly/test_poly.shx (100%) create mode 100644 django/contrib/gis/tests/data/test_vrt/test_vrt.csv create mode 100644 django/contrib/gis/tests/data/test_vrt/test_vrt.vrt diff --git a/django/contrib/gis/gdal/error.py b/django/contrib/gis/gdal/error.py index ffcc4dfed5..4ec3f7e735 100644 --- a/django/contrib/gis/gdal/error.py +++ b/django/contrib/gis/gdal/error.py @@ -25,6 +25,7 @@ OGRERR_DICT = { 1 : (OGRException, 'Not enough data.'), 5 : (OGRException, 'Corrupt data.'), 6 : (OGRException, 'OGR failure.'), 7 : (SRSException, 'Unsupported SRS.'), + 8 : (OGRException, 'Invalid handle.'), } OGRERR_NONE = 0 diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index e76a5add5f..ee0ec17559 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -201,7 +201,13 @@ class OGRGeometry(object): @property def geom_type(self): "Returns the Type for this Geometry." - return OGRGeomType(get_geom_type(self._ptr)) + try: + return OGRGeomType(get_geom_type(self._ptr)) + except OGRException: + # VRT datasources return an invalid geometry type + # number, but a valid name -- we'll try that instead. + # See: http://trac.osgeo.org/gdal/ticket/2491 + return OGRGeomType(get_geom_name(self._ptr)) @property def geom_name(self): diff --git a/django/contrib/gis/gdal/geomtype.py b/django/contrib/gis/gdal/geomtype.py index 3bcd0db9b1..565326f5a8 100644 --- a/django/contrib/gis/gdal/geomtype.py +++ b/django/contrib/gis/gdal/geomtype.py @@ -4,31 +4,42 @@ from django.contrib.gis.gdal.error import OGRException class OGRGeomType(object): "Encapulates OGR Geometry Types." - # Ordered array of acceptable strings and their corresponding OGRwkbGeometryType - __ogr_str = ['Unknown', 'Point', 'LineString', 'Polygon', 'MultiPoint', - 'MultiLineString', 'MultiPolygon', 'GeometryCollection', - 'LinearRing'] - __ogr_int = [0, 1, 2, 3, 4, 5, 6, 7, 101] + # Dictionary of acceptable OGRwkbGeometryType s and their string names. + _types = {0 : 'Unknown', + 1 : 'Point', + 2 : 'LineString', + 3 : 'Polygon', + 4 : 'MultiPoint', + 5 : 'MultiLineString', + 6 : 'MultiPolygon', + 7 : 'GeometryCollection', + 100 : 'None', + 101 : 'LinearRing', + } + # Reverse type dictionary, keyed by lower-case of the name. + _str_types = dict([(v.lower(), k) for k, v in _types.items()]) def __init__(self, type_input): "Figures out the correct OGR Type based upon the input." if isinstance(type_input, OGRGeomType): - self._index = type_input._index + num = type_input.num elif isinstance(type_input, basestring): - idx = self._has_str(self.__ogr_str, type_input) - if idx == None: + num = self._str_types.get(type_input.lower(), None) + if num is None: raise OGRException('Invalid OGR String Type "%s"' % type_input) - self._index = idx elif isinstance(type_input, int): - if not type_input in self.__ogr_int: + if not type_input in self._types: raise OGRException('Invalid OGR Integer Type: %d' % type_input) - self._index = self.__ogr_int.index(type_input) + num = type_input else: raise TypeError('Invalid OGR input type given.') + + # Setting the OGR geometry type number. + self.num = num def __str__(self): - "Returns a short-hand string form of the OGR Geometry type." - return self.__ogr_str[self._index] + "Returns the value of the name property." + return self.name def __eq__(self, other): """ @@ -36,37 +47,27 @@ class OGRGeomType(object): other OGRGeomType, the short-hand string, or the integer. """ if isinstance(other, OGRGeomType): - return self._index == other._index + return self.num == other.num elif isinstance(other, basestring): - idx = self._has_str(self.__ogr_str, other) - if not (idx == None): return self._index == idx - return False + return self.name.lower() == other.lower() elif isinstance(other, int): - if not other in self.__ogr_int: return False - return self.__ogr_int.index(other) == self._index + return self.num == other else: - raise TypeError('Cannot compare with type: %s' % str(type(other))) + return False def __ne__(self, other): return not (self == other) - def _has_str(self, arr, s): - "Case-insensitive search of the string array for the given pattern." - s_low = s.lower() - for i in xrange(len(arr)): - if s_low == arr[i].lower(): return i - return None + @property + def name(self): + "Returns a short-hand string form of the OGR Geometry type." + return self._types[self.num] @property def django(self): "Returns the Django GeometryField for this OGR Type." - s = self.__ogr_str[self._index] - if s in ('Unknown', 'LinearRing'): + s = self.name + if s in ('Unknown', 'LinearRing', 'None'): return None else: return s + 'Field' - - @property - def num(self): - "Returns the OGRwkbGeometryType number for the OGR Type." - return self.__ogr_int[self._index] diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index a41b28819b..d60e2cb7f6 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -14,7 +14,7 @@ from django.contrib.gis.gdal.prototypes.ds import \ get_extent, get_fd_geom_type, get_fd_name, get_feature, get_feature_count, \ get_field_count, get_field_defn, get_field_name, get_field_precision, \ get_field_width, get_field_type, get_layer_defn, get_layer_srs, \ - get_next_feature, reset_reading + get_next_feature, reset_reading, test_capability from django.contrib.gis.gdal.prototypes.srs import clone_srs # For more information, see the OGR C API source code: @@ -32,29 +32,29 @@ class Layer(object): raise OGRException('Cannot create Layer, invalid pointer given') self._ptr = layer_ptr self._ldefn = get_layer_defn(self._ptr) + # Does the Layer support random reading? + self._random_read = self.test_capability('RandomRead') def __getitem__(self, index): "Gets the Feature at the specified index." - if not isinstance(index, (slice, int)): - raise TypeError - end = self.num_feat - if isinstance(index,int): - # An integer index was given - if index < 0: - index = end - index - if index < 0 or index >= self.num_feat: - raise OGRIndexError('index out of range') + if isinstance(index, (int, long)): + # An integer index was given -- we cannot do a check based on the + # number of features because the beginning and ending feature IDs + # are not guaranteed to be 0 and len(layer)-1, respectively. + if index < 0: raise OGRIndexError('Negative indices are not allowed on OGR Layers.') return self._make_feature(index) - else: + elif isinstance(index, slice): # A slice was given - start, stop, stride = index.indices(end) - return [self._make_feature(offset) for offset in range(start,stop,stride)] + start, stop, stride = index.indices(self.num_feat) + return [self._make_feature(fid) for fid in xrange(start, stop, stride)] + else: + raise TypeError('Integers and slices may only be used when indexing OGR Layers.') def __iter__(self): "Iterates over each Feature in the Layer." # ResetReading() must be called before iteration is to begin. reset_reading(self._ptr) - for i in range(self.num_feat): + for i in xrange(self.num_feat): yield Feature(get_next_feature(self._ptr), self._ldefn) def __len__(self): @@ -65,9 +65,26 @@ class Layer(object): "The string name of the layer." return self.name - def _make_feature(self, offset): - "Helper routine for __getitem__ that makes a feature from an offset." - return Feature(get_feature(self._ptr, offset), self._ldefn) + def _make_feature(self, feat_id): + """ + Helper routine for __getitem__ that constructs a Feature from the given + Feature ID. If the OGR Layer does not support random-access reading, + then each feature of the layer will be incremented through until the + a Feature is found matching the given feature ID. + """ + if self._random_read: + # If the Layer supports random reading, return. + try: + return Feature(get_feature(self._ptr, feat_id), self._ldefn) + except OGRException: + pass + else: + # Random access isn't supported, have to increment through + # each feature until the given feature ID is encountered. + for feat in self: + if feat.fid == feat_id: return feat + # Should have returned a Feature, raise an OGRIndexError. + raise OGRIndexError('Invalid feature id: %s.' % feat_id) #### Layer properties #### @property @@ -158,3 +175,13 @@ class Layer(object): return [GEOSGeometry(feat.geom.wkb) for feat in self] else: return [feat.geom for feat in self] + + def test_capability(self, capability): + """ + Returns a bool indicating whether the this Layer supports the given + capability (a string). Valid capability strings include: + 'RandomRead', 'SequentialWrite', 'RandomWrite', 'FastSpatialFilter', + 'FastFeatureCount', 'FastGetExtent', 'CreateField', 'Transactions', + 'DeleteFeature', and 'FastSetNextByIndex'. + """ + return bool(test_capability(self._ptr, capability)) diff --git a/django/contrib/gis/gdal/prototypes/ds.py b/django/contrib/gis/gdal/prototypes/ds.py index 3a7fb05833..b64183eeb3 100644 --- a/django/contrib/gis/gdal/prototypes/ds.py +++ b/django/contrib/gis/gdal/prototypes/ds.py @@ -37,6 +37,7 @@ get_layer_defn = voidptr_output(lgdal.OGR_L_GetLayerDefn, [c_void_p]) get_layer_srs = srs_output(lgdal.OGR_L_GetSpatialRef, [c_void_p]) get_next_feature = voidptr_output(lgdal.OGR_L_GetNextFeature, [c_void_p]) reset_reading = void_output(lgdal.OGR_L_ResetReading, [c_void_p], errcheck=False) +test_capability = int_output(lgdal.OGR_L_TestCapability, [c_void_p, c_char_p]) ### Feature Definition Routines ### get_fd_geom_type = int_output(lgdal.OGR_FD_GetGeomType, [c_void_p]) diff --git a/django/contrib/gis/tests/test_point/test_point.dbf b/django/contrib/gis/tests/data/test_point/test_point.dbf similarity index 100% rename from django/contrib/gis/tests/test_point/test_point.dbf rename to django/contrib/gis/tests/data/test_point/test_point.dbf diff --git a/django/contrib/gis/tests/test_point/test_point.prj b/django/contrib/gis/tests/data/test_point/test_point.prj similarity index 100% rename from django/contrib/gis/tests/test_point/test_point.prj rename to django/contrib/gis/tests/data/test_point/test_point.prj diff --git a/django/contrib/gis/tests/test_point/test_point.shp b/django/contrib/gis/tests/data/test_point/test_point.shp similarity index 100% rename from django/contrib/gis/tests/test_point/test_point.shp rename to django/contrib/gis/tests/data/test_point/test_point.shp diff --git a/django/contrib/gis/tests/test_point/test_point.shx b/django/contrib/gis/tests/data/test_point/test_point.shx similarity index 100% rename from django/contrib/gis/tests/test_point/test_point.shx rename to django/contrib/gis/tests/data/test_point/test_point.shx diff --git a/django/contrib/gis/tests/test_poly/test_poly.dbf b/django/contrib/gis/tests/data/test_poly/test_poly.dbf similarity index 100% rename from django/contrib/gis/tests/test_poly/test_poly.dbf rename to django/contrib/gis/tests/data/test_poly/test_poly.dbf diff --git a/django/contrib/gis/tests/test_poly/test_poly.prj b/django/contrib/gis/tests/data/test_poly/test_poly.prj similarity index 100% rename from django/contrib/gis/tests/test_poly/test_poly.prj rename to django/contrib/gis/tests/data/test_poly/test_poly.prj diff --git a/django/contrib/gis/tests/test_poly/test_poly.shp b/django/contrib/gis/tests/data/test_poly/test_poly.shp similarity index 100% rename from django/contrib/gis/tests/test_poly/test_poly.shp rename to django/contrib/gis/tests/data/test_poly/test_poly.shp diff --git a/django/contrib/gis/tests/test_poly/test_poly.shx b/django/contrib/gis/tests/data/test_poly/test_poly.shx similarity index 100% rename from django/contrib/gis/tests/test_poly/test_poly.shx rename to django/contrib/gis/tests/data/test_poly/test_poly.shx diff --git a/django/contrib/gis/tests/data/test_vrt/test_vrt.csv b/django/contrib/gis/tests/data/test_vrt/test_vrt.csv new file mode 100644 index 0000000000..dff648ffe0 --- /dev/null +++ b/django/contrib/gis/tests/data/test_vrt/test_vrt.csv @@ -0,0 +1,4 @@ +POINT_X,POINT_Y,NUM +1.0,2.0,5 +5.0,23.0,17 +100.0,523.5,23 diff --git a/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt b/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt new file mode 100644 index 0000000000..85e6be8e27 --- /dev/null +++ b/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt @@ -0,0 +1,7 @@ + + +test_vrt.csv +wkbPoint + + + \ No newline at end of file diff --git a/django/contrib/gis/tests/test_gdal_ds.py b/django/contrib/gis/tests/test_gdal_ds.py index 361c762281..9b826859f9 100644 --- a/django/contrib/gis/tests/test_gdal_ds.py +++ b/django/contrib/gis/tests/test_gdal_ds.py @@ -3,27 +3,38 @@ from django.contrib.gis.gdal import DataSource, Envelope, OGRException, OGRIndex from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString # Path for SHP files -shp_path = os.path.dirname(__file__) -def get_shp(name): - return shp_path + os.sep + name + os.sep + name + '.shp' +data_path = os.path.join(os.path.dirname(__file__), 'data') +def get_ds_file(name, ext): + return os.sep.join([data_path, name, name + '.%s' % ext]) # Test SHP data source object -class TestSHP: - def __init__(self, shp, **kwargs): - self.ds = get_shp(shp) +class TestDS: + def __init__(self, name, **kwargs): + ext = kwargs.pop('ext', 'shp') + self.ds = get_ds_file(name, ext) for key, value in kwargs.items(): setattr(self, key, value) # List of acceptable data sources. -ds_list = (TestSHP('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, - extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS - srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), - TestSHP('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, - extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS - srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), +ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', + fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', + field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : range(1, 6), 'str' : [str(i) for i in range(1, 6)]}, + fids=range(5)), + TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype=1, driver='VRT', + fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. + extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV + field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, + fids=range(1,4)), + TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, + driver='ESRI Shapefile', + fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), ) -bad_ds = (TestSHP('foo'), +bad_ds = (TestDS('foo'), ) class DataSourceTest(unittest.TestCase): @@ -42,7 +53,7 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(source.ds, ds.name) # Making sure the driver name matches up - self.assertEqual('ESRI Shapefile', str(ds.driver)) + self.assertEqual(source.driver, str(ds.driver)) # Making sure indexing works try: @@ -57,20 +68,17 @@ class DataSourceTest(unittest.TestCase): for source in bad_ds: self.assertRaises(OGRException, DataSource, source.ds) - def test03_layers(self): + def test03a_layers(self): "Testing Data Source Layers." - + print "\nBEGIN - expecting out of range feature id error; safe to ignore.\n" for source in ds_list: ds = DataSource(source.ds) - # Incrementing through each layer, this tests __iter__ + # Incrementing through each layer, this tests DataSource.__iter__ for layer in ds: # Making sure we get the number of features we expect self.assertEqual(len(layer), source.nfeat) - layer[0] #can index - layer[:1] #can slice - # Making sure we get the number of fields we expect self.assertEqual(source.nfld, layer.num_fields) self.assertEqual(source.nfld, len(layer.fields)) @@ -85,6 +93,42 @@ class DataSourceTest(unittest.TestCase): # Now checking the field names. flds = layer.fields for f in flds: self.assertEqual(True, f in source.fields) + + # Negative FIDs are not allowed. + self.assertRaises(OGRIndexError, layer.__getitem__, -1) + self.assertRaises(OGRIndexError, layer.__getitem__, 50000) + + if hasattr(source, 'field_values'): + fld_names = source.field_values.keys() + + # Testing `Layer.get_fields` (which uses Layer.__iter__) + for fld_name in fld_names: + self.assertEqual(source.field_values[fld_name], layer.get_fields(fld_name)) + + # Testing `Layer.__getitem__`. + for i, fid in enumerate(source.fids): + feat = layer[fid] + self.assertEqual(fid, feat.fid) + # Maybe this should be in the test below, but we might as well test + # the feature values here while in this loop. + for fld_name in fld_names: + self.assertEqual(source.field_values[fld_name][i], feat.get(fld_name)) + print "\nEND - expecting out of range feature id error; safe to ignore." + + def test03b_layer_slice(self): + "Test indexing and slicing on Layers." + # Using the first data-source because the same slice + # can be used for both the layer and the control values. + source = ds_list[0] + ds = DataSource(source.ds) + + sl = slice(1, 3) + feats = ds[0][sl] + + for fld_name in ds[0].fields: + test_vals = [feat.get(fld_name) for feat in feats] + control_vals = source.field_values[fld_name][sl] + self.assertEqual(control_vals, test_vals) def test04_features(self): "Testing Data Source Features." @@ -95,7 +139,8 @@ class DataSourceTest(unittest.TestCase): for layer in ds: # Incrementing through each feature in the layer for feat in layer: - # Making sure the number of fields is what's expected. + # Making sure the number of fields, and the geometry type + # are what's expected. self.assertEqual(source.nfld, len(list(feat))) self.assertEqual(source.gtype, feat.geom_type) @@ -105,7 +150,7 @@ class DataSourceTest(unittest.TestCase): # a string value index for the feature. self.assertEqual(True, isinstance(feat[k], v)) - # Testing __iter__ on the Feature + # Testing Feature.__iter__ for fld in feat: self.assertEqual(True, fld.name in source.fields.keys()) def test05_geometries(self): @@ -123,8 +168,10 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(source.gtype, g.geom_type) # Making sure the SpatialReference is as expected. - self.assertEqual(source.srs_wkt, g.srs.wkt) - + if hasattr(source, 'srs_wkt'): + self.assertEqual(source.srs_wkt, g.srs.wkt) + + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(DataSourceTest)) diff --git a/django/contrib/gis/tests/test_gdal_geom.py b/django/contrib/gis/tests/test_gdal_geom.py index 3775bd46fc..b0dab0e9e8 100644 --- a/django/contrib/gis/tests/test_gdal_geom.py +++ b/django/contrib/gis/tests/test_gdal_geom.py @@ -36,6 +36,11 @@ class OGRGeomTest(unittest.TestCase): self.assertEqual(False, OGRGeomType(1) != OGRGeomType('point')) self.assertEqual(True, OGRGeomType('POINT') != OGRGeomType(6)) + # Testing the Django field name equivalent property. + self.assertEqual('PointField', OGRGeomType('Point').django) + self.assertEqual(None, OGRGeomType('Unknown').django) + self.assertEqual(None, OGRGeomType('none').django) + def test01a_wkt(self): "Testing WKT output." for g in wkt_out: