operations.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """
  2. This module contains the spatial lookup types, and the `get_geo_where_clause`
  3. routine for Oracle Spatial.
  4. Please note that WKT support is broken on the XE version, and thus
  5. this backend will not work on such platforms. Specifically, XE lacks
  6. support for an internal JVM, and Java libraries are required to use
  7. the WKT constructors.
  8. """
  9. import re
  10. from django.contrib.gis.db import models
  11. from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
  12. from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
  13. from django.contrib.gis.db.backends.utils import SpatialOperator
  14. from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
  15. from django.contrib.gis.geos.prototypes.io import wkb_r
  16. from django.contrib.gis.measure import Distance
  17. from django.db.backends.oracle.operations import DatabaseOperations
  18. DEFAULT_TOLERANCE = "0.05"
  19. class SDOOperator(SpatialOperator):
  20. sql_template = "%(func)s(%(lhs)s, %(rhs)s) = 'TRUE'"
  21. class SDODWithin(SpatialOperator):
  22. sql_template = "SDO_WITHIN_DISTANCE(%(lhs)s, %(rhs)s, %%s) = 'TRUE'"
  23. class SDODisjoint(SpatialOperator):
  24. sql_template = (
  25. "SDO_GEOM.RELATE(%%(lhs)s, 'DISJOINT', %%(rhs)s, %s) = 'DISJOINT'"
  26. % DEFAULT_TOLERANCE
  27. )
  28. class SDORelate(SpatialOperator):
  29. sql_template = "SDO_RELATE(%(lhs)s, %(rhs)s, 'mask=%(mask)s') = 'TRUE'"
  30. def check_relate_argument(self, arg):
  31. masks = (
  32. "TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|"
  33. "CONTAINS|COVERS|ANYINTERACT|ON"
  34. )
  35. mask_regex = re.compile(r"^(%s)(\+(%s))*$" % (masks, masks), re.I)
  36. if not isinstance(arg, str) or not mask_regex.match(arg):
  37. raise ValueError('Invalid SDO_RELATE mask: "%s"' % arg)
  38. def as_sql(self, connection, lookup, template_params, sql_params):
  39. template_params["mask"] = sql_params[-1]
  40. return super().as_sql(connection, lookup, template_params, sql_params[:-1])
  41. class OracleOperations(BaseSpatialOperations, DatabaseOperations):
  42. name = "oracle"
  43. oracle = True
  44. disallowed_aggregates = (models.Collect, models.Extent3D, models.MakeLine)
  45. Adapter = OracleSpatialAdapter
  46. extent = "SDO_AGGR_MBR"
  47. unionagg = "SDO_AGGR_UNION"
  48. from_text = "SDO_GEOMETRY"
  49. function_names = {
  50. "Area": "SDO_GEOM.SDO_AREA",
  51. "AsGeoJSON": "SDO_UTIL.TO_GEOJSON",
  52. "AsWKB": "SDO_UTIL.TO_WKBGEOMETRY",
  53. "AsWKT": "SDO_UTIL.TO_WKTGEOMETRY",
  54. "BoundingCircle": "SDO_GEOM.SDO_MBC",
  55. "Centroid": "SDO_GEOM.SDO_CENTROID",
  56. "Difference": "SDO_GEOM.SDO_DIFFERENCE",
  57. "Distance": "SDO_GEOM.SDO_DISTANCE",
  58. "Envelope": "SDO_GEOM_MBR",
  59. "FromWKB": "SDO_UTIL.FROM_WKBGEOMETRY",
  60. "FromWKT": "SDO_UTIL.FROM_WKTGEOMETRY",
  61. "Intersection": "SDO_GEOM.SDO_INTERSECTION",
  62. "IsValid": "SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT",
  63. "Length": "SDO_GEOM.SDO_LENGTH",
  64. "NumGeometries": "SDO_UTIL.GETNUMELEM",
  65. "NumPoints": "SDO_UTIL.GETNUMVERTICES",
  66. "Perimeter": "SDO_GEOM.SDO_LENGTH",
  67. "PointOnSurface": "SDO_GEOM.SDO_POINTONSURFACE",
  68. "Reverse": "SDO_UTIL.REVERSE_LINESTRING",
  69. "SymDifference": "SDO_GEOM.SDO_XOR",
  70. "Transform": "SDO_CS.TRANSFORM",
  71. "Union": "SDO_GEOM.SDO_UNION",
  72. }
  73. # We want to get SDO Geometries as WKT because it is much easier to
  74. # instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
  75. # However, this adversely affects performance (i.e., Java is called
  76. # to convert to WKT on every query). If someone wishes to write a
  77. # SDO_GEOMETRY(...) parser in Python, let me know =)
  78. select = "SDO_UTIL.TO_WKBGEOMETRY(%s)"
  79. gis_operators = {
  80. "contains": SDOOperator(func="SDO_CONTAINS"),
  81. "coveredby": SDOOperator(func="SDO_COVEREDBY"),
  82. "covers": SDOOperator(func="SDO_COVERS"),
  83. "disjoint": SDODisjoint(),
  84. "intersects": SDOOperator(
  85. func="SDO_OVERLAPBDYINTERSECT"
  86. ), # TODO: Is this really the same as ST_Intersects()?
  87. "equals": SDOOperator(func="SDO_EQUAL"),
  88. "exact": SDOOperator(func="SDO_EQUAL"),
  89. "overlaps": SDOOperator(func="SDO_OVERLAPS"),
  90. "same_as": SDOOperator(func="SDO_EQUAL"),
  91. # Oracle uses a different syntax, e.g., 'mask=inside+touch'
  92. "relate": SDORelate(),
  93. "touches": SDOOperator(func="SDO_TOUCH"),
  94. "within": SDOOperator(func="SDO_INSIDE"),
  95. "dwithin": SDODWithin(),
  96. }
  97. unsupported_functions = {
  98. "AsKML",
  99. "AsSVG",
  100. "Azimuth",
  101. "ClosestPoint",
  102. "ForcePolygonCW",
  103. "GeoHash",
  104. "GeometryDistance",
  105. "IsEmpty",
  106. "LineLocatePoint",
  107. "MakeValid",
  108. "MemSize",
  109. "Scale",
  110. "SnapToGrid",
  111. "Translate",
  112. }
  113. def geo_quote_name(self, name):
  114. return super().geo_quote_name(name).upper()
  115. def convert_extent(self, clob):
  116. if clob:
  117. # Generally, Oracle returns a polygon for the extent -- however,
  118. # it can return a single point if there's only one Point in the
  119. # table.
  120. ext_geom = GEOSGeometry(memoryview(clob.read()))
  121. gtype = str(ext_geom.geom_type)
  122. if gtype == "Polygon":
  123. # Construct the 4-tuple from the coordinates in the polygon.
  124. shell = ext_geom.shell
  125. ll, ur = shell[0][:2], shell[2][:2]
  126. elif gtype == "Point":
  127. ll = ext_geom.coords[:2]
  128. ur = ll
  129. else:
  130. raise Exception(
  131. "Unexpected geometry type returned for extent: %s" % gtype
  132. )
  133. xmin, ymin = ll
  134. xmax, ymax = ur
  135. return (xmin, ymin, xmax, ymax)
  136. else:
  137. return None
  138. def geo_db_type(self, f):
  139. """
  140. Return the geometry database type for Oracle. Unlike other spatial
  141. backends, no stored procedure is necessary and it's the same for all
  142. geometry types.
  143. """
  144. return "MDSYS.SDO_GEOMETRY"
  145. def get_distance(self, f, value, lookup_type):
  146. """
  147. Return the distance parameters given the value and the lookup type.
  148. On Oracle, geometry columns with a geodetic coordinate system behave
  149. implicitly like a geography column, and thus meters will be used as
  150. the distance parameter on them.
  151. """
  152. if not value:
  153. return []
  154. value = value[0]
  155. if isinstance(value, Distance):
  156. if f.geodetic(self.connection):
  157. dist_param = value.m
  158. else:
  159. dist_param = getattr(
  160. value, Distance.unit_attname(f.units_name(self.connection))
  161. )
  162. else:
  163. dist_param = value
  164. # dwithin lookups on Oracle require a special string parameter
  165. # that starts with "distance=".
  166. if lookup_type == "dwithin":
  167. dist_param = "distance=%s" % dist_param
  168. return [dist_param]
  169. def get_geom_placeholder(self, f, value, compiler):
  170. if value is None:
  171. return "NULL"
  172. return super().get_geom_placeholder(f, value, compiler)
  173. def spatial_aggregate_name(self, agg_name):
  174. """
  175. Return the spatial aggregate SQL name.
  176. """
  177. agg_name = "unionagg" if agg_name.lower() == "union" else agg_name.lower()
  178. return getattr(self, agg_name)
  179. # Routines for getting the OGC-compliant models.
  180. def geometry_columns(self):
  181. from django.contrib.gis.db.backends.oracle.models import OracleGeometryColumns
  182. return OracleGeometryColumns
  183. def spatial_ref_sys(self):
  184. from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys
  185. return OracleSpatialRefSys
  186. def modify_insert_params(self, placeholder, params):
  187. """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
  188. backend due to #10888.
  189. """
  190. if placeholder == "NULL":
  191. return []
  192. return super().modify_insert_params(placeholder, params)
  193. def get_geometry_converter(self, expression):
  194. read = wkb_r().read
  195. srid = expression.output_field.srid
  196. if srid == -1:
  197. srid = None
  198. geom_class = expression.output_field.geom_class
  199. def converter(value, expression, connection):
  200. if value is not None:
  201. geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
  202. if srid:
  203. geom.srid = srid
  204. return geom
  205. return converter
  206. def get_area_att_for_field(self, field):
  207. return "sq_m"