operations.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. from django.contrib.gis.db.models import GeometryField
  2. from django.contrib.gis.db.models.functions import Distance
  3. from django.contrib.gis.measure import Area as AreaMeasure
  4. from django.contrib.gis.measure import Distance as DistanceMeasure
  5. from django.db import NotSupportedError
  6. from django.utils.functional import cached_property
  7. class BaseSpatialOperations:
  8. # Quick booleans for the type of this spatial backend, and
  9. # an attribute for the spatial database version tuple (if applicable)
  10. postgis = False
  11. spatialite = False
  12. mariadb = False
  13. mysql = False
  14. oracle = False
  15. spatial_version = None
  16. # How the geometry column should be selected.
  17. select = "%s"
  18. @cached_property
  19. def select_extent(self):
  20. return self.select
  21. # Aggregates
  22. disallowed_aggregates = ()
  23. geom_func_prefix = ""
  24. # Mapping between Django function names and backend names, when names do not
  25. # match; used in spatial_function_name().
  26. function_names = {}
  27. # Set of known unsupported functions of the backend
  28. unsupported_functions = {
  29. "Area",
  30. "AsGeoJSON",
  31. "AsGML",
  32. "AsKML",
  33. "AsSVG",
  34. "Azimuth",
  35. "BoundingCircle",
  36. "Centroid",
  37. "Difference",
  38. "Distance",
  39. "Envelope",
  40. "GeoHash",
  41. "GeometryDistance",
  42. "Intersection",
  43. "IsEmpty",
  44. "IsValid",
  45. "Length",
  46. "LineLocatePoint",
  47. "MakeValid",
  48. "MemSize",
  49. "NumGeometries",
  50. "NumPoints",
  51. "Perimeter",
  52. "PointOnSurface",
  53. "Reverse",
  54. "Scale",
  55. "SnapToGrid",
  56. "SymDifference",
  57. "Transform",
  58. "Translate",
  59. "Union",
  60. }
  61. # Constructors
  62. from_text = False
  63. # Default conversion functions for aggregates; will be overridden if implemented
  64. # for the spatial backend.
  65. def convert_extent(self, box, srid):
  66. raise NotImplementedError(
  67. "Aggregate extent not implemented for this spatial backend."
  68. )
  69. def convert_extent3d(self, box, srid):
  70. raise NotImplementedError(
  71. "Aggregate 3D extent not implemented for this spatial backend."
  72. )
  73. # For quoting column values, rather than columns.
  74. def geo_quote_name(self, name):
  75. return "'%s'" % name
  76. # GeometryField operations
  77. def geo_db_type(self, f):
  78. """
  79. Return the database column type for the geometry field on
  80. the spatial backend.
  81. """
  82. raise NotImplementedError(
  83. "subclasses of BaseSpatialOperations must provide a geo_db_type() method"
  84. )
  85. def get_distance(self, f, value, lookup_type):
  86. """
  87. Return the distance parameters for the given geometry field,
  88. lookup value, and lookup type.
  89. """
  90. raise NotImplementedError(
  91. "Distance operations not available on this spatial backend."
  92. )
  93. def get_geom_placeholder(self, f, value, compiler):
  94. """
  95. Return the placeholder for the given geometry field with the given
  96. value. Depending on the spatial backend, the placeholder may contain a
  97. stored procedure call to the transformation function of the spatial
  98. backend.
  99. """
  100. def transform_value(value, field):
  101. return value is not None and value.srid != field.srid
  102. if hasattr(value, "as_sql"):
  103. return (
  104. "%s(%%s, %s)" % (self.spatial_function_name("Transform"), f.srid)
  105. if transform_value(value.output_field, f)
  106. else "%s"
  107. )
  108. if transform_value(value, f):
  109. # Add Transform() to the SQL placeholder.
  110. return "%s(%s(%%s,%s), %s)" % (
  111. self.spatial_function_name("Transform"),
  112. self.from_text,
  113. value.srid,
  114. f.srid,
  115. )
  116. elif self.connection.features.has_spatialrefsys_table:
  117. return "%s(%%s,%s)" % (self.from_text, f.srid)
  118. else:
  119. # For backwards compatibility on MySQL (#27464).
  120. return "%s(%%s)" % self.from_text
  121. def check_expression_support(self, expression):
  122. if isinstance(expression, self.disallowed_aggregates):
  123. raise NotSupportedError(
  124. "%s spatial aggregation is not supported by this database backend."
  125. % expression.name
  126. )
  127. super().check_expression_support(expression)
  128. def spatial_aggregate_name(self, agg_name):
  129. raise NotImplementedError(
  130. "Aggregate support not implemented for this spatial backend."
  131. )
  132. def spatial_function_name(self, func_name):
  133. if func_name in self.unsupported_functions:
  134. raise NotSupportedError(
  135. "This backend doesn't support the %s function." % func_name
  136. )
  137. return self.function_names.get(func_name, self.geom_func_prefix + func_name)
  138. # Routines for getting the OGC-compliant models.
  139. def geometry_columns(self):
  140. raise NotImplementedError(
  141. "Subclasses of BaseSpatialOperations must provide a geometry_columns() "
  142. "method."
  143. )
  144. def spatial_ref_sys(self):
  145. raise NotImplementedError(
  146. "subclasses of BaseSpatialOperations must a provide spatial_ref_sys() "
  147. "method"
  148. )
  149. distance_expr_for_lookup = staticmethod(Distance)
  150. def get_db_converters(self, expression):
  151. converters = super().get_db_converters(expression)
  152. if isinstance(expression.output_field, GeometryField):
  153. converters.append(self.get_geometry_converter(expression))
  154. return converters
  155. def get_geometry_converter(self, expression):
  156. raise NotImplementedError(
  157. "Subclasses of BaseSpatialOperations must provide a "
  158. "get_geometry_converter() method."
  159. )
  160. def get_area_att_for_field(self, field):
  161. if field.geodetic(self.connection):
  162. if self.connection.features.supports_area_geodetic:
  163. return "sq_m"
  164. raise NotImplementedError(
  165. "Area on geodetic coordinate systems not supported."
  166. )
  167. else:
  168. units_name = field.units_name(self.connection)
  169. if units_name:
  170. return AreaMeasure.unit_attname(units_name)
  171. def get_distance_att_for_field(self, field):
  172. dist_att = None
  173. if field.geodetic(self.connection):
  174. if self.connection.features.supports_distance_geodetic:
  175. dist_att = "m"
  176. else:
  177. units = field.units_name(self.connection)
  178. if units:
  179. dist_att = DistanceMeasure.unit_attname(units)
  180. return dist_att