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.1KB

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