Loading django/contrib/gis/db/backends/base/features.py +2 −1 Original line number Diff line number Diff line Loading @@ -26,8 +26,9 @@ class BaseSpatialFeatures(object): supports_real_shape_operations = True # Can geometry fields be null? supports_null_geometries = True # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems? # Can the `distance`/`length` functions be applied on geodetic coordinate systems? supports_distance_geodetic = True supports_length_geodetic = True # Is the database able to count vertices on polygons (with `num_points`)? supports_num_points_poly = True Loading django/contrib/gis/db/backends/spatialite/operations.py +21 −0 Original line number Diff line number Diff line """ SQL functions reference lists: http://www.gaia-gis.it/spatialite-2.4.0/spatialite-sql-2.4.html http://www.gaia-gis.it/spatialite-3.0.0-BETA/spatialite-sql-3.0.0.html http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.1.html """ import re import sys Loading Loading @@ -74,6 +80,21 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): 'distance_lte': SpatialOperator(func='Distance', op='<='), } function_names = { 'Length': 'ST_Length', 'Reverse': 'ST_Reverse', 'Scale': 'ScaleCoords', 'Translate': 'ST_Translate', 'Union': 'ST_Union', } @cached_property def unsupported_functions(self): unsupported = {'BoundingCircle', 'ForceRHR', 'GeoHash', 'MemSize'} if self.spatial_version < (4, 0, 0): unsupported.add('Reverse') return unsupported @cached_property def spatial_version(self): """Determine the version of the SpatiaLite library.""" Loading django/contrib/gis/db/models/functions.py +47 −4 Original line number Diff line number Diff line Loading @@ -79,6 +79,9 @@ class GeomValue(Value): self.value = connection.ops.Adapter(self.value) return super(GeomValue, self).as_sql(compiler, connection) def as_sqlite(self, compiler, connection): return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)] class GeoFuncWithGeoParam(GeoFunc): def __init__(self, expression, geom, *expressions, **extra): Loading @@ -94,6 +97,18 @@ class GeoFuncWithGeoParam(GeoFunc): super(GeoFuncWithGeoParam, self).__init__(expression, geom, *expressions, **extra) class SQLiteDecimalToFloatMixin(object): """ By default, Decimal values are converted to str by the SQLite backend, which is not acceptable by the GIS functions expecting numeric values. """ def as_sqlite(self, compiler, connection): for expr in self.get_source_expressions(): if hasattr(expr, 'value') and isinstance(expr.value, Decimal): expr.value = float(expr.value) return super(SQLiteDecimalToFloatMixin, self).as_sql(compiler, connection) class Area(GeoFunc): def as_sql(self, compiler, connection): if connection.ops.oracle: Loading Loading @@ -143,7 +158,10 @@ class AsGML(GeoFunc): class AsKML(AsGML): pass def as_sqlite(self, compiler, connection): # No version parameter self.source_expressions.pop(0) return super(AsKML, self).as_sql(compiler, connection) class AsSVG(GeoFunc): Loading Loading @@ -261,6 +279,15 @@ class Length(DistanceResultMixin, GeoFunc): self.function = connection.ops.length3d return super(Length, self).as_sql(compiler, connection) def as_sqlite(self, compiler, connection): geo_field = GeometryField(srid=self.srid) if geo_field.geodetic(connection): if self.spheroid: self.function = 'GeodesicLength' else: self.function = 'GreatCircleLength' return super(Length, self).as_sql(compiler, connection) class MemSize(GeoFunc): output_field_class = IntegerField Loading @@ -273,6 +300,11 @@ class NumGeometries(GeoFunc): class NumPoints(GeoFunc): output_field_class = IntegerField def as_sqlite(self, compiler, connection): if self.source_expressions[self.geom_param_pos].output_field.geom_type != 'LINESTRING': raise TypeError("Spatialite NumPoints can only operate on LineString content") return super(NumPoints, self).as_sql(compiler, connection) class Perimeter(DistanceResultMixin, GeoFunc): output_field_class = FloatField Loading @@ -292,7 +324,7 @@ class Reverse(GeoFunc): pass class Scale(GeoFunc): class Scale(SQLiteDecimalToFloatMixin, GeoFunc): def __init__(self, expression, x, y, z=0.0, **extra): expressions = [ expression, Loading @@ -304,7 +336,7 @@ class Scale(GeoFunc): super(Scale, self).__init__(*expressions, **extra) class SnapToGrid(GeoFunc): class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc): def __init__(self, expression, *args, **extra): nargs = len(args) expressions = [expression] Loading Loading @@ -342,9 +374,20 @@ class Transform(GeoFunc): # Make srid the resulting srid of the transformation return self.source_expressions[self.geom_param_pos + 1].value def convert_value(self, value, expression, connection, context): value = super(Transform, self).convert_value(value, expression, connection, context) if not connection.ops.postgis and not value.srid: # Some backends do not set the srid on the returning geometry value.srid = self.srid return value class Translate(Scale): pass def as_sqlite(self, compiler, connection): # Always provide the z parameter if len(self.source_expressions) < 4: self.source_expressions.append(Value(0)) return super(Translate, self).as_sqlite(compiler, connection) class Union(GeoFuncWithGeoParam): Loading tests/gis_tests/distapp/tests.py +4 −2 Original line number Diff line number Diff line Loading @@ -617,13 +617,15 @@ class DistanceFunctionsTests(TestCase): len_m1 = 473504.769553813 len_m2 = 4617.668 if connection.features.supports_distance_geodetic: if connection.features.supports_length_geodetic: qs = Interstate.objects.annotate(length=Length('path')) tol = 2 if oracle else 3 self.assertAlmostEqual(len_m1, qs[0].length.m, tol) # TODO: test with spheroid argument (True and False) else: # Does not support geodetic coordinate systems. self.assertRaises(ValueError, Interstate.objects.annotate(length=Length('path'))) with self.assertRaises(ValueError): Interstate.objects.annotate(length=Length('path')) # Now doing length on a projected coordinate system. i10 = SouthTexasInterstate.objects.annotate(length=Length('path')).get(name='I-10') Loading tests/gis_tests/geoapp/test_functions.py +17 −5 Original line number Diff line number Diff line Loading @@ -172,15 +172,22 @@ class GISFunctionsTests(TestCase): @skipUnlessDBFeature("has_Difference_function") def test_difference(self): geom = Point(5, 23, srid=4326) qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) qs = Country.objects.annotate(diff=functions.Difference('mpoly', geom)) # For some reason SpatiaLite does something screwy with the Texas geometry here. if spatialite: qs = qs.exclude(name='Texas') for c in qs: self.assertEqual(c.mpoly.difference(geom), c.difference) self.assertEqual(c.mpoly.difference(geom), c.diff) @skipUnlessDBFeature("has_Difference_function") def test_difference_mixed_srid(self): """Testing with mixed SRID (Country has default 4326).""" geom = Point(556597.4, 2632018.6, srid=3857) # Spherical mercator qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) # For some reason SpatiaLite does something screwy with the Texas geometry here. if spatialite: qs = qs.exclude(name='Texas') for c in qs: self.assertEqual(c.mpoly.difference(geom), c.difference) Loading Loading @@ -220,7 +227,12 @@ class GISFunctionsTests(TestCase): geom = Point(5, 23, srid=4326) qs = Country.objects.annotate(inter=functions.Intersection('mpoly', geom)) for c in qs: self.assertEqual(c.mpoly.intersection(geom), c.inter) if spatialite: # When the intersection is empty, Spatialite returns None expected = None else: expected = c.mpoly.intersection(geom) self.assertEqual(c.inter, expected) @skipUnlessDBFeature("has_MemSize_function") def test_memsize(self): Loading Loading @@ -416,8 +428,8 @@ class GISFunctionsTests(TestCase): union=functions.Union('mpoly', geom), ) # XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also, # XXX it doesn't like the null intersection. # For some reason SpatiaLite does something screwey with the Texas geometry here. # Also, it doesn't like the null intersection. if spatialite: qs = qs.exclude(name='Texas') else: Loading Loading
django/contrib/gis/db/backends/base/features.py +2 −1 Original line number Diff line number Diff line Loading @@ -26,8 +26,9 @@ class BaseSpatialFeatures(object): supports_real_shape_operations = True # Can geometry fields be null? supports_null_geometries = True # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems? # Can the `distance`/`length` functions be applied on geodetic coordinate systems? supports_distance_geodetic = True supports_length_geodetic = True # Is the database able to count vertices on polygons (with `num_points`)? supports_num_points_poly = True Loading
django/contrib/gis/db/backends/spatialite/operations.py +21 −0 Original line number Diff line number Diff line """ SQL functions reference lists: http://www.gaia-gis.it/spatialite-2.4.0/spatialite-sql-2.4.html http://www.gaia-gis.it/spatialite-3.0.0-BETA/spatialite-sql-3.0.0.html http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.1.html """ import re import sys Loading Loading @@ -74,6 +80,21 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): 'distance_lte': SpatialOperator(func='Distance', op='<='), } function_names = { 'Length': 'ST_Length', 'Reverse': 'ST_Reverse', 'Scale': 'ScaleCoords', 'Translate': 'ST_Translate', 'Union': 'ST_Union', } @cached_property def unsupported_functions(self): unsupported = {'BoundingCircle', 'ForceRHR', 'GeoHash', 'MemSize'} if self.spatial_version < (4, 0, 0): unsupported.add('Reverse') return unsupported @cached_property def spatial_version(self): """Determine the version of the SpatiaLite library.""" Loading
django/contrib/gis/db/models/functions.py +47 −4 Original line number Diff line number Diff line Loading @@ -79,6 +79,9 @@ class GeomValue(Value): self.value = connection.ops.Adapter(self.value) return super(GeomValue, self).as_sql(compiler, connection) def as_sqlite(self, compiler, connection): return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)] class GeoFuncWithGeoParam(GeoFunc): def __init__(self, expression, geom, *expressions, **extra): Loading @@ -94,6 +97,18 @@ class GeoFuncWithGeoParam(GeoFunc): super(GeoFuncWithGeoParam, self).__init__(expression, geom, *expressions, **extra) class SQLiteDecimalToFloatMixin(object): """ By default, Decimal values are converted to str by the SQLite backend, which is not acceptable by the GIS functions expecting numeric values. """ def as_sqlite(self, compiler, connection): for expr in self.get_source_expressions(): if hasattr(expr, 'value') and isinstance(expr.value, Decimal): expr.value = float(expr.value) return super(SQLiteDecimalToFloatMixin, self).as_sql(compiler, connection) class Area(GeoFunc): def as_sql(self, compiler, connection): if connection.ops.oracle: Loading Loading @@ -143,7 +158,10 @@ class AsGML(GeoFunc): class AsKML(AsGML): pass def as_sqlite(self, compiler, connection): # No version parameter self.source_expressions.pop(0) return super(AsKML, self).as_sql(compiler, connection) class AsSVG(GeoFunc): Loading Loading @@ -261,6 +279,15 @@ class Length(DistanceResultMixin, GeoFunc): self.function = connection.ops.length3d return super(Length, self).as_sql(compiler, connection) def as_sqlite(self, compiler, connection): geo_field = GeometryField(srid=self.srid) if geo_field.geodetic(connection): if self.spheroid: self.function = 'GeodesicLength' else: self.function = 'GreatCircleLength' return super(Length, self).as_sql(compiler, connection) class MemSize(GeoFunc): output_field_class = IntegerField Loading @@ -273,6 +300,11 @@ class NumGeometries(GeoFunc): class NumPoints(GeoFunc): output_field_class = IntegerField def as_sqlite(self, compiler, connection): if self.source_expressions[self.geom_param_pos].output_field.geom_type != 'LINESTRING': raise TypeError("Spatialite NumPoints can only operate on LineString content") return super(NumPoints, self).as_sql(compiler, connection) class Perimeter(DistanceResultMixin, GeoFunc): output_field_class = FloatField Loading @@ -292,7 +324,7 @@ class Reverse(GeoFunc): pass class Scale(GeoFunc): class Scale(SQLiteDecimalToFloatMixin, GeoFunc): def __init__(self, expression, x, y, z=0.0, **extra): expressions = [ expression, Loading @@ -304,7 +336,7 @@ class Scale(GeoFunc): super(Scale, self).__init__(*expressions, **extra) class SnapToGrid(GeoFunc): class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc): def __init__(self, expression, *args, **extra): nargs = len(args) expressions = [expression] Loading Loading @@ -342,9 +374,20 @@ class Transform(GeoFunc): # Make srid the resulting srid of the transformation return self.source_expressions[self.geom_param_pos + 1].value def convert_value(self, value, expression, connection, context): value = super(Transform, self).convert_value(value, expression, connection, context) if not connection.ops.postgis and not value.srid: # Some backends do not set the srid on the returning geometry value.srid = self.srid return value class Translate(Scale): pass def as_sqlite(self, compiler, connection): # Always provide the z parameter if len(self.source_expressions) < 4: self.source_expressions.append(Value(0)) return super(Translate, self).as_sqlite(compiler, connection) class Union(GeoFuncWithGeoParam): Loading
tests/gis_tests/distapp/tests.py +4 −2 Original line number Diff line number Diff line Loading @@ -617,13 +617,15 @@ class DistanceFunctionsTests(TestCase): len_m1 = 473504.769553813 len_m2 = 4617.668 if connection.features.supports_distance_geodetic: if connection.features.supports_length_geodetic: qs = Interstate.objects.annotate(length=Length('path')) tol = 2 if oracle else 3 self.assertAlmostEqual(len_m1, qs[0].length.m, tol) # TODO: test with spheroid argument (True and False) else: # Does not support geodetic coordinate systems. self.assertRaises(ValueError, Interstate.objects.annotate(length=Length('path'))) with self.assertRaises(ValueError): Interstate.objects.annotate(length=Length('path')) # Now doing length on a projected coordinate system. i10 = SouthTexasInterstate.objects.annotate(length=Length('path')).get(name='I-10') Loading
tests/gis_tests/geoapp/test_functions.py +17 −5 Original line number Diff line number Diff line Loading @@ -172,15 +172,22 @@ class GISFunctionsTests(TestCase): @skipUnlessDBFeature("has_Difference_function") def test_difference(self): geom = Point(5, 23, srid=4326) qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) qs = Country.objects.annotate(diff=functions.Difference('mpoly', geom)) # For some reason SpatiaLite does something screwy with the Texas geometry here. if spatialite: qs = qs.exclude(name='Texas') for c in qs: self.assertEqual(c.mpoly.difference(geom), c.difference) self.assertEqual(c.mpoly.difference(geom), c.diff) @skipUnlessDBFeature("has_Difference_function") def test_difference_mixed_srid(self): """Testing with mixed SRID (Country has default 4326).""" geom = Point(556597.4, 2632018.6, srid=3857) # Spherical mercator qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) # For some reason SpatiaLite does something screwy with the Texas geometry here. if spatialite: qs = qs.exclude(name='Texas') for c in qs: self.assertEqual(c.mpoly.difference(geom), c.difference) Loading Loading @@ -220,7 +227,12 @@ class GISFunctionsTests(TestCase): geom = Point(5, 23, srid=4326) qs = Country.objects.annotate(inter=functions.Intersection('mpoly', geom)) for c in qs: self.assertEqual(c.mpoly.intersection(geom), c.inter) if spatialite: # When the intersection is empty, Spatialite returns None expected = None else: expected = c.mpoly.intersection(geom) self.assertEqual(c.inter, expected) @skipUnlessDBFeature("has_MemSize_function") def test_memsize(self): Loading Loading @@ -416,8 +428,8 @@ class GISFunctionsTests(TestCase): union=functions.Union('mpoly', geom), ) # XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also, # XXX it doesn't like the null intersection. # For some reason SpatiaLite does something screwey with the Texas geometry here. # Also, it doesn't like the null intersection. if spatialite: qs = qs.exclude(name='Texas') else: Loading