operations.py 8.3 KB

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