diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py index ecefde493b..13da494814 100644 --- a/django/contrib/gis/db/backends/base/features.py +++ b/django/contrib/gis/db/backends/base/features.py @@ -14,6 +14,8 @@ class BaseSpatialFeatures(object): # Does the backend introspect GeometryField to its subtypes? supports_geometry_field_introspection = True + # Does the backend support storing 3D geometries? + supports_3d_storage = False # Reference implementation of 3D functions is: # http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions supports_3d_functions = False diff --git a/django/contrib/gis/db/backends/postgis/features.py b/django/contrib/gis/db/backends/postgis/features.py index 1cc15bde38..181c859641 100644 --- a/django/contrib/gis/db/backends/postgis/features.py +++ b/django/contrib/gis/db/backends/postgis/features.py @@ -4,5 +4,6 @@ from django.db.backends.postgresql_psycopg2.features import \ class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures): + supports_3d_storage = True supports_3d_functions = True supports_left_right_lookups = True diff --git a/django/contrib/gis/db/backends/spatialite/features.py b/django/contrib/gis/db/backends/spatialite/features.py index 613087b51e..5bc4b88de8 100644 --- a/django/contrib/gis/db/backends/spatialite/features.py +++ b/django/contrib/gis/db/backends/spatialite/features.py @@ -1,4 +1,5 @@ from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.contrib.gis.geos import geos_version from django.db.backends.sqlite3.features import \ DatabaseFeatures as SQLiteDatabaseFeatures from django.utils.functional import cached_property @@ -15,3 +16,7 @@ class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures): # which can result in a significant performance improvement when # creating the database. return self.connection.ops.spatial_version >= (4, 1, 0) + + @cached_property + def supports_3d_storage(self): + return geos_version() >= '3.3' diff --git a/django/contrib/gis/db/backends/spatialite/introspection.py b/django/contrib/gis/db/backends/spatialite/introspection.py index c1ed7629b9..4222c04f8b 100644 --- a/django/contrib/gis/db/backends/spatialite/introspection.py +++ b/django/contrib/gis/db/backends/spatialite/introspection.py @@ -41,7 +41,13 @@ class SpatiaLiteIntrospection(DatabaseIntrospection): # OGRGeomType does not require GDAL and makes it easy to convert # from OGC geom type name to Django field. - field_type = OGRGeomType(row[2]).django + ogr_type = row[2] + if isinstance(ogr_type, six.integer_types) and ogr_type > 1000: + # Spatialite versions >= 4 use the new SFSQL 1.2 offsets + # 1000 (Z), 2000 (M), and 3000 (ZM) to indicate the presence of + # higher dimensional coordinates (M not yet supported by Django). + ogr_type = ogr_type % 1000 + OGRGeomType.wkb25bit + field_type = OGRGeomType(ogr_type).django # Getting any GeometryField keyword arguments that are not the default. dim = row[0] @@ -49,7 +55,7 @@ class SpatiaLiteIntrospection(DatabaseIntrospection): field_params = {} if srid != 4326: field_params['srid'] = srid - if isinstance(dim, six.string_types) and 'Z' in dim: + if (isinstance(dim, six.string_types) and 'Z' in dim) or dim == 3: field_params['dim'] = 3 finally: cursor.close() diff --git a/tests/gis_tests/geo3d/tests.py b/tests/gis_tests/geo3d/tests.py index 022a8063ab..b05201d0bf 100644 --- a/tests/gis_tests/geo3d/tests.py +++ b/tests/gis_tests/geo3d/tests.py @@ -74,7 +74,7 @@ bbox_data = ( @skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.") -@skipUnlessDBFeature("gis_enabled", "supports_3d_functions") +@skipUnlessDBFeature("gis_enabled", "supports_3d_storage") class Geo3DTest(TestCase): """ Only a subset of the PostGIS routines are 3D-enabled, and this TestCase @@ -189,6 +189,7 @@ class Geo3DTest(TestCase): ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$') self.assertTrue(ref_json_regex.match(h.geojson)) + @skipUnlessDBFeature("supports_3d_functions") def test_union(self): """ Testing the Union aggregate of 3D models. @@ -207,6 +208,7 @@ class Geo3DTest(TestCase): # Ordering of points in the resulting geometry may vary between implementations self.assertSetEqual({p.ewkt for p in ref_union}, {p.ewkt for p in union}) + @skipUnlessDBFeature("supports_3d_functions") @ignore_warnings(category=RemovedInDjango20Warning) def test_extent(self): """ @@ -227,6 +229,7 @@ class Geo3DTest(TestCase): self.assertIsNone(City3D.objects.none().extent3d()) self.assertIsNone(City3D.objects.none().aggregate(Extent3D('point'))['point__extent3d']) + @skipUnlessDBFeature("supports_3d_functions") def test_perimeter(self): """ Testing GeoQuerySet.perimeter() on 3D fields. @@ -244,6 +247,7 @@ class Geo3DTest(TestCase): Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m, tol) + @skipUnlessDBFeature("supports_3d_functions") def test_length(self): """ Testing GeoQuerySet.length() on 3D fields. @@ -276,6 +280,7 @@ class Geo3DTest(TestCase): InterstateProj3D.objects.length().get(name='I-45').length.m, tol) + @skipUnlessDBFeature("supports_3d_functions") def test_scale(self): """ Testing GeoQuerySet.scale() on Z values. @@ -287,6 +292,7 @@ class Geo3DTest(TestCase): for city in City3D.objects.scale(1.0, 1.0, zscale): self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z) + @skipUnlessDBFeature("supports_3d_functions") def test_translate(self): """ Testing GeoQuerySet.translate() on Z values. diff --git a/tests/gis_tests/inspectapp/models.py b/tests/gis_tests/inspectapp/models.py index a649605487..fd343b7803 100644 --- a/tests/gis_tests/inspectapp/models.py +++ b/tests/gis_tests/inspectapp/models.py @@ -14,3 +14,11 @@ class AllOGRFields(models.Model): point = models.PointField() objects = models.GeoManager() + + +class Fields3D(models.Model): + point = models.PointField(dim=3) + line = models.LineStringField(dim=3) + poly = models.PolygonField(dim=3) + + objects = models.GeoManager() diff --git a/tests/gis_tests/inspectapp/tests.py b/tests/gis_tests/inspectapp/tests.py index 19d5aa054c..c40ffaa7e8 100644 --- a/tests/gis_tests/inspectapp/tests.py +++ b/tests/gis_tests/inspectapp/tests.py @@ -27,9 +27,11 @@ class InspectDbTests(TestCase): Test the geo-enabled inspectdb command. """ out = StringIO() - call_command('inspectdb', - table_name_filter=lambda tn: tn.startswith('inspectapp_'), - stdout=out) + call_command( + 'inspectdb', + table_name_filter=lambda tn: tn == 'inspectapp_allogrfields', + stdout=out + ) output = out.getvalue() if connection.features.supports_geometry_field_introspection: self.assertIn('geom = models.PolygonField()', output) @@ -39,6 +41,25 @@ class InspectDbTests(TestCase): self.assertIn('point = models.GeometryField(', output) self.assertIn('objects = models.GeoManager()', output) + @skipUnlessDBFeature("supports_3d_storage") + def test_3d_columns(self): + out = StringIO() + call_command( + 'inspectdb', + table_name_filter=lambda tn: tn == 'inspectapp_fields3d', + stdout=out + ) + output = out.getvalue() + if connection.features.supports_geometry_field_introspection: + self.assertIn('point = models.PointField(dim=3)', output) + self.assertIn('line = models.LineStringField(dim=3)', output) + self.assertIn('poly = models.PolygonField(dim=3)', output) + else: + self.assertIn('point = models.GeometryField(', output) + self.assertIn('line = models.GeometryField(', output) + self.assertIn('poly = models.GeometryField(', output) + self.assertIn('objects = models.GeoManager()', output) + @skipUnless(HAS_GDAL, "OGRInspectTest needs GDAL support") @skipUnlessDBFeature("gis_enabled")