/django/contrib/gis/db/backends/oracle/operations.py

https://code.google.com/p/mango-py/ · Python · 293 lines · 248 code · 21 blank · 24 comment · 13 complexity · 0c4e7b51f1aa50f30bdc4ea2deb3f5e0 MD5 · raw file

  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 decimal import Decimal
  11. from django.db.backends.oracle.base import DatabaseOperations
  12. from django.contrib.gis.db.backends.base import BaseSpatialOperations
  13. from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
  14. from django.contrib.gis.db.backends.util import SpatialFunction
  15. from django.contrib.gis.geometry.backend import Geometry
  16. from django.contrib.gis.measure import Distance
  17. class SDOOperation(SpatialFunction):
  18. "Base class for SDO* Oracle operations."
  19. sql_template = "%(function)s(%(geo_col)s, %(geometry)s) %(operator)s '%(result)s'"
  20. def __init__(self, func, **kwargs):
  21. kwargs.setdefault('operator', '=')
  22. kwargs.setdefault('result', 'TRUE')
  23. super(SDOOperation, self).__init__(func, **kwargs)
  24. class SDODistance(SpatialFunction):
  25. "Class for Distance queries."
  26. sql_template = ('%(function)s(%(geo_col)s, %(geometry)s, %(tolerance)s) '
  27. '%(operator)s %(result)s')
  28. dist_func = 'SDO_GEOM.SDO_DISTANCE'
  29. def __init__(self, op, tolerance=0.05):
  30. super(SDODistance, self).__init__(self.dist_func,
  31. tolerance=tolerance,
  32. operator=op, result='%s')
  33. class SDODWithin(SpatialFunction):
  34. dwithin_func = 'SDO_WITHIN_DISTANCE'
  35. sql_template = "%(function)s(%(geo_col)s, %(geometry)s, %%s) = 'TRUE'"
  36. def __init__(self):
  37. super(SDODWithin, self).__init__(self.dwithin_func)
  38. class SDOGeomRelate(SpatialFunction):
  39. "Class for using SDO_GEOM.RELATE."
  40. relate_func = 'SDO_GEOM.RELATE'
  41. sql_template = ("%(function)s(%(geo_col)s, '%(mask)s', %(geometry)s, "
  42. "%(tolerance)s) %(operator)s '%(mask)s'")
  43. def __init__(self, mask, tolerance=0.05):
  44. # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
  45. # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
  46. super(SDOGeomRelate, self).__init__(self.relate_func, operator='=',
  47. mask=mask, tolerance=tolerance)
  48. class SDORelate(SpatialFunction):
  49. "Class for using SDO_RELATE."
  50. masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
  51. mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
  52. sql_template = "%(function)s(%(geo_col)s, %(geometry)s, 'mask=%(mask)s') = 'TRUE'"
  53. relate_func = 'SDO_RELATE'
  54. def __init__(self, mask):
  55. if not self.mask_regex.match(mask):
  56. raise ValueError('Invalid %s mask: "%s"' % (self.relate_func, mask))
  57. super(SDORelate, self).__init__(self.relate_func, mask=mask)
  58. # Valid distance types and substitutions
  59. dtypes = (Decimal, Distance, float, int, long)
  60. class OracleOperations(DatabaseOperations, BaseSpatialOperations):
  61. compiler_module = "django.contrib.gis.db.backends.oracle.compiler"
  62. name = 'oracle'
  63. oracle = True
  64. valid_aggregates = dict([(a, None) for a in ('Union', 'Extent')])
  65. Adapter = OracleSpatialAdapter
  66. Adaptor = Adapter # Backwards-compatibility alias.
  67. area = 'SDO_GEOM.SDO_AREA'
  68. gml= 'SDO_UTIL.TO_GMLGEOMETRY'
  69. centroid = 'SDO_GEOM.SDO_CENTROID'
  70. difference = 'SDO_GEOM.SDO_DIFFERENCE'
  71. distance = 'SDO_GEOM.SDO_DISTANCE'
  72. extent= 'SDO_AGGR_MBR'
  73. intersection= 'SDO_GEOM.SDO_INTERSECTION'
  74. length = 'SDO_GEOM.SDO_LENGTH'
  75. num_geom = 'SDO_UTIL.GETNUMELEM'
  76. num_points = 'SDO_UTIL.GETNUMVERTICES'
  77. perimeter = length
  78. point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE'
  79. reverse = 'SDO_UTIL.REVERSE_LINESTRING'
  80. sym_difference = 'SDO_GEOM.SDO_XOR'
  81. transform = 'SDO_CS.TRANSFORM'
  82. union = 'SDO_GEOM.SDO_UNION'
  83. unionagg = 'SDO_AGGR_UNION'
  84. # We want to get SDO Geometries as WKT because it is much easier to
  85. # instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
  86. # However, this adversely affects performance (i.e., Java is called
  87. # to convert to WKT on every query). If someone wishes to write a
  88. # SDO_GEOMETRY(...) parser in Python, let me know =)
  89. select = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
  90. distance_functions = {
  91. 'distance_gt' : (SDODistance('>'), dtypes),
  92. 'distance_gte' : (SDODistance('>='), dtypes),
  93. 'distance_lt' : (SDODistance('<'), dtypes),
  94. 'distance_lte' : (SDODistance('<='), dtypes),
  95. 'dwithin' : (SDODWithin(), dtypes),
  96. }
  97. geometry_functions = {
  98. 'contains' : SDOOperation('SDO_CONTAINS'),
  99. 'coveredby' : SDOOperation('SDO_COVEREDBY'),
  100. 'covers' : SDOOperation('SDO_COVERS'),
  101. 'disjoint' : SDOGeomRelate('DISJOINT'),
  102. 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
  103. 'equals' : SDOOperation('SDO_EQUAL'),
  104. 'exact' : SDOOperation('SDO_EQUAL'),
  105. 'overlaps' : SDOOperation('SDO_OVERLAPS'),
  106. 'same_as' : SDOOperation('SDO_EQUAL'),
  107. 'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
  108. 'touches' : SDOOperation('SDO_TOUCH'),
  109. 'within' : SDOOperation('SDO_INSIDE'),
  110. }
  111. geometry_functions.update(distance_functions)
  112. gis_terms = ['isnull']
  113. gis_terms += geometry_functions.keys()
  114. gis_terms = dict([(term, None) for term in gis_terms])
  115. truncate_params = {'relate' : None}
  116. def __init__(self, connection):
  117. super(OracleOperations, self).__init__()
  118. self.connection = connection
  119. def convert_extent(self, clob):
  120. if clob:
  121. # Generally, Oracle returns a polygon for the extent -- however,
  122. # it can return a single point if there's only one Point in the
  123. # table.
  124. ext_geom = Geometry(clob.read())
  125. gtype = str(ext_geom.geom_type)
  126. if gtype == 'Polygon':
  127. # Construct the 4-tuple from the coordinates in the polygon.
  128. shell = ext_geom.shell
  129. ll, ur = shell[0][:2], shell[2][:2]
  130. elif gtype == 'Point':
  131. ll = ext_geom.coords[:2]
  132. ur = ll
  133. else:
  134. raise Exception('Unexpected geometry type returned for extent: %s' % gtype)
  135. xmin, ymin = ll
  136. xmax, ymax = ur
  137. return (xmin, ymin, xmax, ymax)
  138. else:
  139. return None
  140. def convert_geom(self, clob, geo_field):
  141. if clob:
  142. return Geometry(clob.read(), geo_field.srid)
  143. else:
  144. return None
  145. def geo_db_type(self, f):
  146. """
  147. Returns the geometry database type for Oracle. Unlike other spatial
  148. backends, no stored procedure is necessary and it's the same for all
  149. geometry types.
  150. """
  151. return 'MDSYS.SDO_GEOMETRY'
  152. def get_distance(self, f, value, lookup_type):
  153. """
  154. Returns the distance parameters given the value and the lookup type.
  155. On Oracle, geometry columns with a geodetic coordinate system behave
  156. implicitly like a geography column, and thus meters will be used as
  157. the distance parameter on them.
  158. """
  159. if not value:
  160. return []
  161. value = value[0]
  162. if isinstance(value, Distance):
  163. if f.geodetic(self.connection):
  164. dist_param = value.m
  165. else:
  166. dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
  167. else:
  168. dist_param = value
  169. # dwithin lookups on oracle require a special string parameter
  170. # that starts with "distance=".
  171. if lookup_type == 'dwithin':
  172. dist_param = 'distance=%s' % dist_param
  173. return [dist_param]
  174. def get_geom_placeholder(self, f, value):
  175. """
  176. Provides a proper substitution value for Geometries that are not in the
  177. SRID of the field. Specifically, this routine will substitute in the
  178. SDO_CS.TRANSFORM() function call.
  179. """
  180. if value is None:
  181. return 'NULL'
  182. def transform_value(val, srid):
  183. return val.srid != srid
  184. if hasattr(value, 'expression'):
  185. if transform_value(value, f.srid):
  186. placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
  187. else:
  188. placeholder = '%s'
  189. # No geometry value used for F expression, substitue in
  190. # the column name instead.
  191. return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
  192. else:
  193. if transform_value(value, f.srid):
  194. return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid)
  195. else:
  196. return 'SDO_GEOMETRY(%%s, %s)' % f.srid
  197. def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
  198. "Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
  199. alias, col, db_type = lvalue
  200. # Getting the quoted table name as `geo_col`.
  201. geo_col = '%s.%s' % (qn(alias), qn(col))
  202. # See if a Oracle Geometry function matches the lookup type next
  203. lookup_info = self.geometry_functions.get(lookup_type, False)
  204. if lookup_info:
  205. # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
  206. # 'dwithin' lookup types.
  207. if isinstance(lookup_info, tuple):
  208. # First element of tuple is lookup type, second element is the type
  209. # of the expected argument (e.g., str, float)
  210. sdo_op, arg_type = lookup_info
  211. geom = value[0]
  212. # Ensuring that a tuple _value_ was passed in from the user
  213. if not isinstance(value, tuple):
  214. raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
  215. if len(value) != 2:
  216. raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
  217. # Ensuring the argument type matches what we expect.
  218. if not isinstance(value[1], arg_type):
  219. raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
  220. if lookup_type == 'relate':
  221. # The SDORelate class handles construction for these queries,
  222. # and verifies the mask argument.
  223. return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(field, geom))
  224. else:
  225. # Otherwise, just call the `as_sql` method on the SDOOperation instance.
  226. return sdo_op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
  227. else:
  228. # Lookup info is a SDOOperation instance, whose `as_sql` method returns
  229. # the SQL necessary for the geometry function call. For example:
  230. # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
  231. return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value))
  232. elif lookup_type == 'isnull':
  233. # Handling 'isnull' lookup type
  234. return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
  235. raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
  236. def spatial_aggregate_sql(self, agg):
  237. """
  238. Returns the spatial aggregate SQL template and function for the
  239. given Aggregate instance.
  240. """
  241. agg_name = agg.__class__.__name__.lower()
  242. if agg_name == 'union' : agg_name += 'agg'
  243. if agg.is_extent:
  244. sql_template = '%(function)s(%(field)s)'
  245. else:
  246. sql_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))'
  247. sql_function = getattr(self, agg_name)
  248. return self.select % sql_template, sql_function
  249. # Routines for getting the OGC-compliant models.
  250. def geometry_columns(self):
  251. from django.contrib.gis.db.backends.oracle.models import GeometryColumns
  252. return GeometryColumns
  253. def spatial_ref_sys(self):
  254. from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
  255. return SpatialRefSys