|
1 """ |
|
2 This module contains the spatial lookup types, and the `get_geo_where_clause` |
|
3 routine for Oracle Spatial. |
|
4 |
|
5 Please note that WKT support is broken on the XE version, and thus |
|
6 this backend will not work on such platforms. Specifically, XE lacks |
|
7 support for an internal JVM, and Java libraries are required to use |
|
8 the WKT constructors. |
|
9 """ |
|
10 import re |
|
11 from decimal import Decimal |
|
12 from django.db import connection |
|
13 from django.contrib.gis.db.backend.util import SpatialFunction |
|
14 from django.contrib.gis.measure import Distance |
|
15 qn = connection.ops.quote_name |
|
16 |
|
17 # The GML, distance, transform, and union procedures. |
|
18 AREA = 'SDO_GEOM.SDO_AREA' |
|
19 ASGML = 'SDO_UTIL.TO_GMLGEOMETRY' |
|
20 CENTROID = 'SDO_GEOM.SDO_CENTROID' |
|
21 DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE' |
|
22 DISTANCE = 'SDO_GEOM.SDO_DISTANCE' |
|
23 EXTENT = 'SDO_AGGR_MBR' |
|
24 INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION' |
|
25 LENGTH = 'SDO_GEOM.SDO_LENGTH' |
|
26 NUM_GEOM = 'SDO_UTIL.GETNUMELEM' |
|
27 NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES' |
|
28 POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE' |
|
29 SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR' |
|
30 TRANSFORM = 'SDO_CS.TRANSFORM' |
|
31 UNION = 'SDO_GEOM.SDO_UNION' |
|
32 UNIONAGG = 'SDO_AGGR_UNION' |
|
33 |
|
34 # We want to get SDO Geometries as WKT because it is much easier to |
|
35 # instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings. |
|
36 # However, this adversely affects performance (i.e., Java is called |
|
37 # to convert to WKT on every query). If someone wishes to write a |
|
38 # SDO_GEOMETRY(...) parser in Python, let me know =) |
|
39 GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' |
|
40 |
|
41 #### Classes used in constructing Oracle spatial SQL #### |
|
42 class SDOOperation(SpatialFunction): |
|
43 "Base class for SDO* Oracle operations." |
|
44 def __init__(self, func, **kwargs): |
|
45 kwargs.setdefault('operator', '=') |
|
46 kwargs.setdefault('result', 'TRUE') |
|
47 kwargs.setdefault('end_subst', ") %s '%s'") |
|
48 super(SDOOperation, self).__init__(func, **kwargs) |
|
49 |
|
50 class SDODistance(SpatialFunction): |
|
51 "Class for Distance queries." |
|
52 def __init__(self, op, tolerance=0.05): |
|
53 super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance, |
|
54 operator=op, result='%%s') |
|
55 |
|
56 class SDOGeomRelate(SpatialFunction): |
|
57 "Class for using SDO_GEOM.RELATE." |
|
58 def __init__(self, mask, tolerance=0.05): |
|
59 # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance. |
|
60 # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE'). |
|
61 end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask) |
|
62 beg_subst = "%%s(%%s, '%s'" % mask |
|
63 super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst) |
|
64 |
|
65 class SDORelate(SpatialFunction): |
|
66 "Class for using SDO_RELATE." |
|
67 masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON' |
|
68 mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I) |
|
69 def __init__(self, mask): |
|
70 func = 'SDO_RELATE' |
|
71 if not self.mask_regex.match(mask): |
|
72 raise ValueError('Invalid %s mask: "%s"' % (func, mask)) |
|
73 super(SDORelate, self).__init__(func, end_subst=", 'mask=%s') = 'TRUE'" % mask) |
|
74 |
|
75 #### Lookup type mapping dictionaries of Oracle spatial operations #### |
|
76 |
|
77 # Valid distance types and substitutions |
|
78 dtypes = (Decimal, Distance, float, int, long) |
|
79 DISTANCE_FUNCTIONS = { |
|
80 'distance_gt' : (SDODistance('>'), dtypes), |
|
81 'distance_gte' : (SDODistance('>='), dtypes), |
|
82 'distance_lt' : (SDODistance('<'), dtypes), |
|
83 'distance_lte' : (SDODistance('<='), dtypes), |
|
84 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', |
|
85 beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes), |
|
86 } |
|
87 |
|
88 ORACLE_GEOMETRY_FUNCTIONS = { |
|
89 'contains' : SDOOperation('SDO_CONTAINS'), |
|
90 'coveredby' : SDOOperation('SDO_COVEREDBY'), |
|
91 'covers' : SDOOperation('SDO_COVERS'), |
|
92 'disjoint' : SDOGeomRelate('DISJOINT'), |
|
93 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? |
|
94 'equals' : SDOOperation('SDO_EQUAL'), |
|
95 'exact' : SDOOperation('SDO_EQUAL'), |
|
96 'overlaps' : SDOOperation('SDO_OVERLAPS'), |
|
97 'same_as' : SDOOperation('SDO_EQUAL'), |
|
98 'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch' |
|
99 'touches' : SDOOperation('SDO_TOUCH'), |
|
100 'within' : SDOOperation('SDO_INSIDE'), |
|
101 } |
|
102 ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) |
|
103 |
|
104 # This lookup type does not require a mapping. |
|
105 MISC_TERMS = ['isnull'] |
|
106 |
|
107 # Acceptable lookup types for Oracle spatial. |
|
108 ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys() |
|
109 ORACLE_SPATIAL_TERMS += MISC_TERMS |
|
110 ORACLE_SPATIAL_TERMS = dict((term, None) for term in ORACLE_SPATIAL_TERMS) # Making dictionary for fast lookups |
|
111 |
|
112 #### The `get_geo_where_clause` function for Oracle #### |
|
113 def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): |
|
114 "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." |
|
115 # Getting the quoted table name as `geo_col`. |
|
116 geo_col = '%s.%s' % (qn(table_alias), qn(name)) |
|
117 |
|
118 # See if a Oracle Geometry function matches the lookup type next |
|
119 lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) |
|
120 if lookup_info: |
|
121 # Lookup types that are tuples take tuple arguments, e.g., 'relate' and |
|
122 # 'dwithin' lookup types. |
|
123 if isinstance(lookup_info, tuple): |
|
124 # First element of tuple is lookup type, second element is the type |
|
125 # of the expected argument (e.g., str, float) |
|
126 sdo_op, arg_type = lookup_info |
|
127 |
|
128 # Ensuring that a tuple _value_ was passed in from the user |
|
129 if not isinstance(geo_annot.value, tuple): |
|
130 raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) |
|
131 if len(geo_annot.value) != 2: |
|
132 raise ValueError('2-element tuple required for %s lookup type.' % lookup_type) |
|
133 |
|
134 # Ensuring the argument type matches what we expect. |
|
135 if not isinstance(geo_annot.value[1], arg_type): |
|
136 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) |
|
137 |
|
138 if lookup_type == 'relate': |
|
139 # The SDORelate class handles construction for these queries, |
|
140 # and verifies the mask argument. |
|
141 return sdo_op(geo_annot.value[1]).as_sql(geo_col) |
|
142 else: |
|
143 # Otherwise, just call the `as_sql` method on the SDOOperation instance. |
|
144 return sdo_op.as_sql(geo_col) |
|
145 else: |
|
146 # Lookup info is a SDOOperation instance, whose `as_sql` method returns |
|
147 # the SQL necessary for the geometry function call. For example: |
|
148 # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' |
|
149 return lookup_info.as_sql(geo_col) |
|
150 elif lookup_type == 'isnull': |
|
151 # Handling 'isnull' lookup type |
|
152 return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) |
|
153 |
|
154 raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) |