operations.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. # Returns true if B's bounding box completely contains A's bounding box.
  43. 'contained': SpatialOperator(func='MbrWithin'),
  44. # Returns true if A's bounding box completely contains B's bounding box.
  45. 'bbcontains': SpatialOperator(func='MbrContains'),
  46. # Returns true if A's bounding box overlaps B's bounding box.
  47. 'bboverlaps': SpatialOperator(func='MbrOverlaps'),
  48. # These are implemented here as synonyms for Equals
  49. 'same_as': SpatialiteNullCheckOperator(func='Equals'),
  50. 'exact': SpatialiteNullCheckOperator(func='Equals'),
  51. # Distance predicates
  52. 'dwithin': SpatialOperator(func='PtDistWithin'),
  53. }
  54. disallowed_aggregates = (aggregates.Extent3D,)
  55. @cached_property
  56. def select(self):
  57. return 'CAST (AsEWKB(%s) AS BLOB)' if self.spatial_version >= (4, 3, 0) else 'AsText(%s)'
  58. function_names = {
  59. 'ForcePolygonCW': 'ST_ForceLHR',
  60. 'Length': 'ST_Length',
  61. 'LineLocatePoint': 'ST_Line_Locate_Point',
  62. 'NumPoints': 'ST_NPoints',
  63. 'Reverse': 'ST_Reverse',
  64. 'Scale': 'ScaleCoords',
  65. 'Translate': 'ST_Translate',
  66. 'Union': 'ST_Union',
  67. }
  68. @cached_property
  69. def unsupported_functions(self):
  70. unsupported = {'BoundingCircle', 'ForceRHR', 'MemSize'}
  71. if not self.lwgeom_version():
  72. unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'}
  73. return unsupported
  74. @cached_property
  75. def spatial_version(self):
  76. """Determine the version of the SpatiaLite library."""
  77. try:
  78. version = self.spatialite_version_tuple()[1:]
  79. except Exception as exc:
  80. raise ImproperlyConfigured(
  81. 'Cannot determine the SpatiaLite version for the "%s" database. '
  82. 'Was the SpatiaLite initialization SQL loaded on this database?' % (
  83. self.connection.settings_dict['NAME'],
  84. )
  85. ) from exc
  86. if version < (4, 1, 0):
  87. raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions 4.1.0 and above.')
  88. return version
  89. def convert_extent(self, box):
  90. """
  91. Convert the polygon data received from SpatiaLite to min/max values.
  92. """
  93. if box is None:
  94. return None
  95. shell = GEOSGeometry(box).shell
  96. xmin, ymin = shell[0][:2]
  97. xmax, ymax = shell[2][:2]
  98. return (xmin, ymin, xmax, ymax)
  99. def geo_db_type(self, f):
  100. """
  101. Return None because geometry columns are added via the
  102. `AddGeometryColumn` stored procedure on SpatiaLite.
  103. """
  104. return None
  105. def get_distance(self, f, value, lookup_type):
  106. """
  107. Return the distance parameters for the given geometry field,
  108. lookup value, and lookup type.
  109. """
  110. if not value:
  111. return []
  112. value = value[0]
  113. if isinstance(value, Distance):
  114. if f.geodetic(self.connection):
  115. if lookup_type == 'dwithin':
  116. raise ValueError(
  117. 'Only numeric values of degree units are allowed on '
  118. 'geographic DWithin queries.'
  119. )
  120. dist_param = value.m
  121. else:
  122. dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
  123. else:
  124. dist_param = value
  125. return [dist_param]
  126. def _get_spatialite_func(self, func):
  127. """
  128. Helper routine for calling SpatiaLite functions and returning
  129. their result.
  130. Any error occurring in this method should be handled by the caller.
  131. """
  132. cursor = self.connection._cursor()
  133. try:
  134. cursor.execute('SELECT %s' % func)
  135. row = cursor.fetchone()
  136. finally:
  137. cursor.close()
  138. return row[0]
  139. def geos_version(self):
  140. "Return the version of GEOS used by SpatiaLite as a string."
  141. return self._get_spatialite_func('geos_version()')
  142. def proj4_version(self):
  143. "Return the version of the PROJ.4 library used by SpatiaLite."
  144. return self._get_spatialite_func('proj4_version()')
  145. def lwgeom_version(self):
  146. """Return the version of LWGEOM library used by SpatiaLite."""
  147. return self._get_spatialite_func('lwgeom_version()')
  148. def spatialite_version(self):
  149. "Return the SpatiaLite library version as a string."
  150. return self._get_spatialite_func('spatialite_version()')
  151. def spatialite_version_tuple(self):
  152. """
  153. Return the SpatiaLite version as a tuple (version string, major,
  154. minor, subminor).
  155. """
  156. version = self.spatialite_version()
  157. return (version,) + get_version_tuple(version)
  158. def spatial_aggregate_name(self, agg_name):
  159. """
  160. Return the spatial aggregate SQL template and function for the
  161. given Aggregate instance.
  162. """
  163. agg_name = 'unionagg' if agg_name.lower() == 'union' else agg_name.lower()
  164. return getattr(self, agg_name)
  165. # Routines for getting the OGC-compliant models.
  166. def geometry_columns(self):
  167. from django.contrib.gis.db.backends.spatialite.models import SpatialiteGeometryColumns
  168. return SpatialiteGeometryColumns
  169. def spatial_ref_sys(self):
  170. from django.contrib.gis.db.backends.spatialite.models import SpatialiteSpatialRefSys
  171. return SpatialiteSpatialRefSys
  172. def get_geometry_converter(self, expression):
  173. geom_class = expression.output_field.geom_class
  174. if self.spatial_version >= (4, 3, 0):
  175. read = wkb_r().read
  176. def converter(value, expression, connection):
  177. return None if value is None else GEOSGeometryBase(read(value), geom_class)
  178. else:
  179. read = wkt_r().read
  180. srid = expression.output_field.srid
  181. if srid == -1:
  182. srid = None
  183. def converter(value, expression, connection):
  184. if value is not None:
  185. geom = GEOSGeometryBase(read(value), geom_class)
  186. if srid:
  187. geom.srid = srid
  188. return geom
  189. return converter