Commit 44bdbbc3 authored by Claude Paroz's avatar Claude Paroz
Browse files

Added Spatialite support to GIS functions

parent d9ff5ef3
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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

+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

@@ -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."""
+47 −4
Original line number Diff line number Diff line
@@ -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):
@@ -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:
@@ -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):
@@ -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
@@ -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
@@ -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,
@@ -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]
@@ -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):
+4 −2
Original line number Diff line number Diff line
@@ -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')
+17 −5
Original line number Diff line number Diff line
@@ -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)

@@ -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):
@@ -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: