You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

operations.py 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """
  2. SQL functions reference lists:
  3. https://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.1.html
  4. """
  5. from django.contrib.gis.db.backends.base.operations import (
  6. BaseSpatialOperations,
  7. )
  8. from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
  9. from django.contrib.gis.db.backends.utils import SpatialOperator
  10. from django.contrib.gis.db.models import aggregates
  11. from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
  12. from django.contrib.gis.geos.prototypes.io import wkb_r, wkt_r
  13. from django.contrib.gis.measure import Distance
  14. from django.core.exceptions import ImproperlyConfigured
  15. from django.db.backends.sqlite3.operations import DatabaseOperations
  16. from django.utils.functional import cached_property
  17. from django.utils.version import get_version_tuple
  18. class SpatialiteNullCheckOperator(SpatialOperator):
  19. def as_sql(self, connection, lookup, template_params, sql_params):
  20. sql, params = super().as_sql(connection, lookup, template_params, sql_params)
  21. return '%s > 0' % sql, params
  22. class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
  23. name = 'spatialite'
  24. spatialite = True
  25. Adapter = SpatiaLiteAdapter
  26. collect = 'Collect'
  27. extent = 'Extent'
  28. makeline = 'MakeLine'
  29. unionagg = 'GUnion'
  30. from_text = 'GeomFromText'
  31. gis_operators = {
  32. # Binary predicates
  33. 'equals': SpatialiteNullCheckOperator(func='Equals'),
  34. 'disjoint': SpatialiteNullCheckOperator(func='Disjoint'),
  35. 'touches': SpatialiteNullCheckOperator(func='Touches'),
  36. 'crosses': SpatialiteNullCheckOperator(func='Crosses'),
  37. 'within': SpatialiteNullCheckOperator(func='Within'),
  38. 'overlaps': SpatialiteNullCheckOperator(func='Overlaps'),
  39. 'contains': SpatialiteNullCheckOperator(func='Contains'),
  40. 'intersects': SpatialiteNullCheckOperator(func='Intersects'),
  41. 'relate': SpatialiteNullCheckOperator(func='Relate'),
  42. 'coveredby': SpatialiteNullCheckOperator(func='CoveredBy'),
  43. 'covers': SpatialiteNullCheckOperator(func='Covers'),
  44. # Returns true if B's bounding box completely contains A's bounding box.
  45. 'contained': SpatialOperator(func='MbrWithin'),
  46. # Returns true if A's bounding box completely contains B's bounding box.
  47. 'bbcontains': SpatialOperator(func='MbrContains'),
  48. # Returns true if A's bounding box overlaps B's bounding box.
  49. 'bboverlaps': SpatialOperator(func='MbrOverlaps'),
  50. # These are implemented here as synonyms for Equals
  51. 'same_as': SpatialiteNullCheckOperator(func='Equals'),
  52. 'exact': SpatialiteNullCheckOperator(func='Equals'),
  53. # Distance predicates
  54. 'dwithin': SpatialOperator(func='PtDistWithin'),
  55. }
  56. disallowed_aggregates = (aggregates.Extent3D,)
  57. @cached_property
  58. def select(self):
  59. return 'CAST (AsEWKB(%s) AS BLOB)' if self.spatial_version >= (4, 3, 0) else 'AsText(%s)'
  60. function_names = {
  61. 'ForcePolygonCW': 'ST_ForceLHR',
  62. 'Length': 'ST_Length',
  63. 'LineLocatePoint': 'ST_Line_Locate_Point',
  64. 'NumPoints': 'ST_NPoints',
  65. 'Reverse': 'ST_Reverse',
  66. 'Scale': 'ScaleCoords',
  67. 'Translate': 'ST_Translate',
  68. 'Union': 'ST_Union',
  69. }
  70. @cached_property
  71. def unsupported_functions(self):
  72. unsupported = {'BoundingCircle', 'ForceRHR', 'MemSize'}
  73. if not self.lwgeom_version():
  74. unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'}
  75. return unsupported
  76. @cached_property
  77. def spatial_version(self):
  78. """Determine the version of the SpatiaLite library."""
  79. try:
  80. version = self.spatialite_version_tuple()[1:]
  81. except Exception as exc:
  82. raise ImproperlyConfigured(
  83. 'Cannot determine the SpatiaLite version for the "%s" database. '
  84. 'Was the SpatiaLite initialization SQL loaded on this database?' % (
  85. self.connection.settings_dict['NAME'],
  86. )
  87. ) from exc
  88. if version < (4, 1, 0):
  89. raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions 4.1.0 and above.')
  90. return version
  91. def convert_extent(self, box):
  92. """
  93. Convert the polygon data received from SpatiaLite to min/max values.
  94. """
  95. if box is None:
  96. return None
  97. shell = GEOSGeometry(box).shell
  98. xmin, ymin = shell[0][:2]
  99. xmax, ymax = shell[2][:2]
  100. return (xmin, ymin, xmax, ymax)
  101. def geo_db_type(self, f):
  102. """
  103. Return None because geometry columns are added via the
  104. `AddGeometryColumn` stored procedure on SpatiaLite.
  105. """
  106. return None
  107. def get_distance(self, f, value, lookup_type):
  108. """
  109. Return the distance parameters for the given geometry field,
  110. lookup value, and lookup type.
  111. """
  112. if not value:
  113. return []
  114. value = value[0]
  115. if isinstance(value, Distance):
  116. if f.geodetic(self.connection):
  117. if lookup_type == 'dwithin':
  118. raise ValueError(
  119. 'Only numeric values of degree units are allowed on '
  120. 'geographic DWithin queries.'
  121. )
  122. dist_param = value.m
  123. else:
  124. dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
  125. else:
  126. dist_param = value
  127. return [dist_param]
  128. def _get_spatialite_func(self, func):
  129. """
  130. Helper routine for calling SpatiaLite functions and returning
  131. their result.
  132. Any error occurring in this method should be handled by the caller.
  133. """
  134. cursor = self.connection._cursor()
  135. try:
  136. cursor.execute('SELECT %s' % func)
  137. row = cursor.fetchone()
  138. finally:
  139. cursor.close()
  140. return row[0]
  141. def geos_version(self):
  142. "Return the version of GEOS used by SpatiaLite as a string."
  143. return self._get_spatialite_func('geos_version()')
  144. def proj4_version(self):
  145. "Return the version of the PROJ.4 library used by SpatiaLite."
  146. return self._get_spatialite_func('proj4_version()')
  147. def lwgeom_version(self):
  148. """Return the version of LWGEOM library used by SpatiaLite."""
  149. return self._get_spatialite_func('lwgeom_version()')
  150. def spatialite_version(self):
  151. "Return the SpatiaLite library version as a string."
  152. return self._get_spatialite_func('spatialite_version()')
  153. def spatialite_version_tuple(self):
  154. """
  155. Return the SpatiaLite version as a tuple (version string, major,
  156. minor, subminor).
  157. """
  158. version = self.spatialite_version()
  159. return (version,) + get_version_tuple(version)
  160. def spatial_aggregate_name(self, agg_name):
  161. """
  162. Return the spatial aggregate SQL template and function for the
  163. given Aggregate instance.
  164. """
  165. agg_name = 'unionagg' if agg_name.lower() == 'union' else agg_name.lower()
  166. return getattr(self, agg_name)
  167. # Routines for getting the OGC-compliant models.
  168. def geometry_columns(self):
  169. from django.contrib.gis.db.backends.spatialite.models import SpatialiteGeometryColumns
  170. return SpatialiteGeometryColumns
  171. def spatial_ref_sys(self):
  172. from django.contrib.gis.db.backends.spatialite.models import SpatialiteSpatialRefSys
  173. return SpatialiteSpatialRefSys
  174. def get_geometry_converter(self, expression):
  175. geom_class = expression.output_field.geom_class
  176. if self.spatial_version >= (4, 3, 0):
  177. read = wkb_r().read
  178. def converter(value, expression, connection):
  179. return None if value is None else GEOSGeometryBase(read(value), geom_class)
  180. else:
  181. read = wkt_r().read
  182. srid = expression.output_field.srid
  183. if srid == -1:
  184. srid = None
  185. def converter(value, expression, connection):
  186. if value is not None:
  187. geom = GEOSGeometryBase(read(value), geom_class)
  188. if srid:
  189. geom.srid = srid
  190. return geom
  191. return converter