1 """ |
|
2 This module contains the spatial lookup types, and the get_geo_where_clause() |
|
3 routine for SpatiaLite. |
|
4 """ |
|
5 import re |
|
6 from decimal import Decimal |
|
7 from django.db import connection |
|
8 from django.contrib.gis.measure import Distance |
|
9 from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction |
|
10 qn = connection.ops.quote_name |
|
11 |
|
12 GEOM_SELECT = 'AsText(%s)' |
|
13 |
|
14 # Dummy func, in case we need it later: |
|
15 def get_func(str): |
|
16 return str |
|
17 |
|
18 # Functions used by the GeoManager & GeoQuerySet |
|
19 AREA = get_func('Area') |
|
20 ASSVG = get_func('AsSVG') |
|
21 CENTROID = get_func('Centroid') |
|
22 CONTAINED = get_func('MbrWithin') |
|
23 DIFFERENCE = get_func('Difference') |
|
24 DISTANCE = get_func('Distance') |
|
25 ENVELOPE = get_func('Envelope') |
|
26 GEOM_FROM_TEXT = get_func('GeomFromText') |
|
27 GEOM_FROM_WKB = get_func('GeomFromWKB') |
|
28 INTERSECTION = get_func('Intersection') |
|
29 LENGTH = get_func('GLength') # OpenGis defines Length, but this conflicts with an SQLite reserved keyword |
|
30 NUM_GEOM = get_func('NumGeometries') |
|
31 NUM_POINTS = get_func('NumPoints') |
|
32 POINT_ON_SURFACE = get_func('PointOnSurface') |
|
33 SCALE = get_func('ScaleCoords') |
|
34 SYM_DIFFERENCE = get_func('SymDifference') |
|
35 TRANSFORM = get_func('Transform') |
|
36 TRANSLATE = get_func('ShiftCoords') |
|
37 UNION = 'GUnion'# OpenGis defines Union, but this conflicts with an SQLite reserved keyword |
|
38 UNIONAGG = 'GUnion' |
|
39 |
|
40 #### Classes used in constructing SpatiaLite spatial SQL #### |
|
41 class SpatiaLiteOperator(SpatialOperation): |
|
42 "For SpatiaLite operators (e.g. `&&`, `~`)." |
|
43 def __init__(self, operator): |
|
44 super(SpatiaLiteOperator, self).__init__(operator=operator, beg_subst='%s %s %%s') |
|
45 |
|
46 class SpatiaLiteFunction(SpatialFunction): |
|
47 "For SpatiaLite function calls." |
|
48 def __init__(self, function, **kwargs): |
|
49 super(SpatiaLiteFunction, self).__init__(get_func(function), **kwargs) |
|
50 |
|
51 class SpatiaLiteFunctionParam(SpatiaLiteFunction): |
|
52 "For SpatiaLite functions that take another parameter." |
|
53 def __init__(self, func): |
|
54 super(SpatiaLiteFunctionParam, self).__init__(func, end_subst=', %%s)') |
|
55 |
|
56 class SpatiaLiteDistance(SpatiaLiteFunction): |
|
57 "For SpatiaLite distance operations." |
|
58 dist_func = 'Distance' |
|
59 def __init__(self, operator): |
|
60 super(SpatiaLiteDistance, self).__init__(self.dist_func, end_subst=') %s %s', |
|
61 operator=operator, result='%%s') |
|
62 |
|
63 class SpatiaLiteRelate(SpatiaLiteFunctionParam): |
|
64 "For SpatiaLite Relate(<geom>, <pattern>) calls." |
|
65 pattern_regex = re.compile(r'^[012TF\*]{9}$') |
|
66 def __init__(self, pattern): |
|
67 if not self.pattern_regex.match(pattern): |
|
68 raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) |
|
69 super(SpatiaLiteRelate, self).__init__('Relate') |
|
70 |
|
71 |
|
72 SPATIALITE_GEOMETRY_FUNCTIONS = { |
|
73 'equals' : SpatiaLiteFunction('Equals'), |
|
74 'disjoint' : SpatiaLiteFunction('Disjoint'), |
|
75 'touches' : SpatiaLiteFunction('Touches'), |
|
76 'crosses' : SpatiaLiteFunction('Crosses'), |
|
77 'within' : SpatiaLiteFunction('Within'), |
|
78 'overlaps' : SpatiaLiteFunction('Overlaps'), |
|
79 'contains' : SpatiaLiteFunction('Contains'), |
|
80 'intersects' : SpatiaLiteFunction('Intersects'), |
|
81 'relate' : (SpatiaLiteRelate, basestring), |
|
82 # Retruns true if B's bounding box completely contains A's bounding box. |
|
83 'contained' : SpatiaLiteFunction('MbrWithin'), |
|
84 # Returns true if A's bounding box completely contains B's bounding box. |
|
85 'bbcontains' : SpatiaLiteFunction('MbrContains'), |
|
86 # Returns true if A's bounding box overlaps B's bounding box. |
|
87 'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'), |
|
88 # These are implemented here as synonyms for Equals |
|
89 'same_as' : SpatiaLiteFunction('Equals'), |
|
90 'exact' : SpatiaLiteFunction('Equals'), |
|
91 } |
|
92 |
|
93 # Valid distance types and substitutions |
|
94 dtypes = (Decimal, Distance, float, int, long) |
|
95 def get_dist_ops(operator): |
|
96 "Returns operations for regular distances; spherical distances are not currently supported." |
|
97 return (SpatiaLiteDistance(operator),) |
|
98 DISTANCE_FUNCTIONS = { |
|
99 'distance_gt' : (get_dist_ops('>'), dtypes), |
|
100 'distance_gte' : (get_dist_ops('>='), dtypes), |
|
101 'distance_lt' : (get_dist_ops('<'), dtypes), |
|
102 'distance_lte' : (get_dist_ops('<='), dtypes), |
|
103 } |
|
104 |
|
105 # Distance functions are a part of SpatiaLite geometry functions. |
|
106 SPATIALITE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) |
|
107 |
|
108 # Any other lookup types that do not require a mapping. |
|
109 MISC_TERMS = ['isnull'] |
|
110 |
|
111 # These are the SpatiaLite-customized QUERY_TERMS -- a list of the lookup types |
|
112 # allowed for geographic queries. |
|
113 SPATIALITE_TERMS = SPATIALITE_GEOMETRY_FUNCTIONS.keys() # Getting the Geometry Functions |
|
114 SPATIALITE_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') |
|
115 SPATIALITE_TERMS = dict((term, None) for term in SPATIALITE_TERMS) # Making a dictionary for fast lookups |
|
116 |
|
117 #### The `get_geo_where_clause` function for SpatiaLite. #### |
|
118 def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): |
|
119 "Returns the SQL WHERE clause for use in SpatiaLite SQL construction." |
|
120 # Getting the quoted field as `geo_col`. |
|
121 geo_col = '%s.%s' % (qn(table_alias), qn(name)) |
|
122 if lookup_type in SPATIALITE_GEOMETRY_FUNCTIONS: |
|
123 # See if a SpatiaLite geometry function matches the lookup type. |
|
124 tmp = SPATIALITE_GEOMETRY_FUNCTIONS[lookup_type] |
|
125 |
|
126 # Lookup types that are tuples take tuple arguments, e.g., 'relate' and |
|
127 # distance lookups. |
|
128 if isinstance(tmp, tuple): |
|
129 # First element of tuple is the SpatiaLiteOperation instance, and the |
|
130 # second element is either the type or a tuple of acceptable types |
|
131 # that may passed in as further parameters for the lookup type. |
|
132 op, arg_type = tmp |
|
133 |
|
134 # Ensuring that a tuple _value_ was passed in from the user |
|
135 if not isinstance(geo_annot.value, (tuple, list)): |
|
136 raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) |
|
137 |
|
138 # Number of valid tuple parameters depends on the lookup type. |
|
139 if len(geo_annot.value) != 2: |
|
140 raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) |
|
141 |
|
142 # Ensuring the argument type matches what we expect. |
|
143 if not isinstance(geo_annot.value[1], arg_type): |
|
144 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) |
|
145 |
|
146 # For lookup type `relate`, the op instance is not yet created (has |
|
147 # to be instantiated here to check the pattern parameter). |
|
148 if lookup_type == 'relate': |
|
149 op = op(geo_annot.value[1]) |
|
150 elif lookup_type in DISTANCE_FUNCTIONS: |
|
151 op = op[0] |
|
152 else: |
|
153 op = tmp |
|
154 # Calling the `as_sql` function on the operation instance. |
|
155 return op.as_sql(geo_col) |
|
156 elif lookup_type == 'isnull': |
|
157 # Handling 'isnull' lookup type |
|
158 return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) |
|
159 |
|
160 raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) |
|