Added Spatialite support to GIS functions
This commit is contained in:
parent
d9ff5ef36d
commit
44bdbbc316
|
@ -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
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
"""
|
||||
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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue