2 from decimal
import Decimal
4 from django
.contrib
.gis
.db
.backends
.base
import BaseSpatialOperations
5 from django
.contrib
.gis
.db
.backends
.util
import SpatialOperation
, SpatialFunction
6 from django
.contrib
.gis
.db
.backends
.spatialite
.adapter
import SpatiaLiteAdapter
7 from django
.contrib
.gis
.geometry
.backend
import Geometry
8 from django
.contrib
.gis
.measure
import Distance
9 from django
.core
.exceptions
import ImproperlyConfigured
10 from django
.db
.backends
.sqlite3
.base
import DatabaseOperations
11 from django
.db
.utils
import DatabaseError
13 class SpatiaLiteOperator(SpatialOperation
):
14 "For SpatiaLite operators (e.g. `&&`, `~`)."
15 def __init__(self
, operator
):
16 super(SpatiaLiteOperator
, self
).__init
__(operator
=operator
)
18 class SpatiaLiteFunction(SpatialFunction
):
19 "For SpatiaLite function calls."
20 def __init__(self
, function
, **kwargs
):
21 super(SpatiaLiteFunction
, self
).__init
__(function
, **kwargs
)
23 class SpatiaLiteFunctionParam(SpatiaLiteFunction
):
24 "For SpatiaLite functions that take another parameter."
25 sql_template
= '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
27 class SpatiaLiteDistance(SpatiaLiteFunction
):
28 "For SpatiaLite distance operations."
29 dist_func
= 'Distance'
30 sql_template
= '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
32 def __init__(self
, operator
):
33 super(SpatiaLiteDistance
, self
).__init
__(self
.dist_func
,
36 class SpatiaLiteRelate(SpatiaLiteFunctionParam
):
37 "For SpatiaLite Relate(<geom>, <pattern>) calls."
38 pattern_regex
= re
.compile(r
'^[012TF\*]{9}$')
39 def __init__(self
, pattern
):
40 if not self
.pattern_regex
.match(pattern
):
41 raise ValueError('Invalid intersection matrix pattern "%s".' % pattern
)
42 super(SpatiaLiteRelate
, self
).__init
__('Relate')
44 # Valid distance types and substitutions
45 dtypes
= (Decimal
, Distance
, float, int, long)
46 def get_dist_ops(operator
):
47 "Returns operations for regular distances; spherical distances are not currently supported."
48 return (SpatiaLiteDistance(operator
),)
50 class SpatiaLiteOperations(DatabaseOperations
, BaseSpatialOperations
):
51 compiler_module
= 'django.contrib.gis.db.models.sql.compiler'
54 version_regex
= re
.compile(r
'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
55 valid_aggregates
= dict([(k
, None) for k
in ('Extent', 'Union')])
57 Adapter
= SpatiaLiteAdapter
58 Adaptor
= Adapter
# Backwards-compatibility alias.
62 contained
= 'MbrWithin'
63 difference
= 'Difference'
66 intersection
= 'Intersection'
67 length
= 'GLength' # OpenGis defines Length, but this conflicts with an SQLite reserved keyword
68 num_geom
= 'NumGeometries'
69 num_points
= 'NumPoints'
70 point_on_surface
= 'PointOnSurface'
73 sym_difference
= 'SymDifference'
74 transform
= 'Transform'
75 translate
= 'ShiftCoords'
76 union
= 'GUnion' # OpenGis defines Union, but this conflicts with an SQLite reserved keyword
79 from_text
= 'GeomFromText'
80 from_wkb
= 'GeomFromWKB'
83 geometry_functions
= {
84 'equals' : SpatiaLiteFunction('Equals'),
85 'disjoint' : SpatiaLiteFunction('Disjoint'),
86 'touches' : SpatiaLiteFunction('Touches'),
87 'crosses' : SpatiaLiteFunction('Crosses'),
88 'within' : SpatiaLiteFunction('Within'),
89 'overlaps' : SpatiaLiteFunction('Overlaps'),
90 'contains' : SpatiaLiteFunction('Contains'),
91 'intersects' : SpatiaLiteFunction('Intersects'),
92 'relate' : (SpatiaLiteRelate
, basestring
),
93 # Retruns true if B's bounding box completely contains A's bounding box.
94 'contained' : SpatiaLiteFunction('MbrWithin'),
95 # Returns true if A's bounding box completely contains B's bounding box.
96 'bbcontains' : SpatiaLiteFunction('MbrContains'),
97 # Returns true if A's bounding box overlaps B's bounding box.
98 'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'),
99 # These are implemented here as synonyms for Equals
100 'same_as' : SpatiaLiteFunction('Equals'),
101 'exact' : SpatiaLiteFunction('Equals'),
104 distance_functions
= {
105 'distance_gt' : (get_dist_ops('>'), dtypes
),
106 'distance_gte' : (get_dist_ops('>='), dtypes
),
107 'distance_lt' : (get_dist_ops('<'), dtypes
),
108 'distance_lte' : (get_dist_ops('<='), dtypes
),
110 geometry_functions
.update(distance_functions
)
112 def __init__(self
, connection
):
113 super(DatabaseOperations
, self
).__init
__()
114 self
.connection
= connection
116 # Determine the version of the SpatiaLite library.
118 vtup
= self
.spatialite_version_tuple()
120 if version
< (2, 3, 0):
121 raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions '
123 self
.spatial_version
= version
124 except ImproperlyConfigured
:
126 except Exception, msg
:
127 raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" '
128 'database (error was "%s"). Was the SpatiaLite initialization '
129 'SQL loaded on this database?' %
130 (self
.connection
.settings_dict
['NAME'], msg
))
132 # Creating the GIS terms dictionary.
133 gis_terms
= ['isnull']
134 gis_terms
+= self
.geometry_functions
.keys()
135 self
.gis_terms
= dict([(term
, None) for term
in gis_terms
])
137 def check_aggregate_support(self
, aggregate
):
139 Checks if the given aggregate name is supported (that is, if it's
140 in `self.valid_aggregates`).
142 agg_name
= aggregate
.__class
__.__name
__
143 return agg_name
in self
.valid_aggregates
145 def convert_geom(self
, wkt
, geo_field
):
147 Converts geometry WKT returned from a SpatiaLite aggregate.
150 return Geometry(wkt
, geo_field
.srid
)
154 def geo_db_type(self
, f
):
156 Returns None because geometry columnas are added via the
157 `AddGeometryColumn` stored procedure on SpatiaLite.
161 def get_distance(self
, f
, value
, lookup_type
):
163 Returns the distance parameters for the given geometry field,
164 lookup value, and lookup type. SpatiaLite only supports regular
165 cartesian-based queries (no spheroid/sphere calculations for point
166 geometries like PostGIS).
171 if isinstance(value
, Distance
):
172 if f
.geodetic(self
.connection
):
173 raise ValueError('SpatiaLite does not support distance queries on '
174 'geometry fields with a geodetic coordinate system. '
175 'Distance objects; use a numeric value of your '
176 'distance in degrees instead.')
178 dist_param
= getattr(value
, Distance
.unit_attname(f
.units_name(self
.connection
)))
183 def get_geom_placeholder(self
, f
, value
):
185 Provides a proper substitution value for Geometries that are not in the
186 SRID of the field. Specifically, this routine will substitute in the
187 Transform() and GeomFromText() function call(s).
189 def transform_value(value
, srid
):
190 return not (value
is None or value
.srid
== srid
)
191 if hasattr(value
, 'expression'):
192 if transform_value(value
, f
.srid
):
193 placeholder
= '%s(%%s, %s)' % (self
.transform
, f
.srid
)
196 # No geometry value used for F expression, substitue in
197 # the column name instead.
198 return placeholder
% '%s.%s' % tuple(map(self
.quote_name
, value
.cols
[value
.expression
]))
200 if transform_value(value
, f
.srid
):
201 # Adding Transform() to the SQL placeholder.
202 return '%s(%s(%%s,%s), %s)' % (self
.transform
, self
.from_text
, value
.srid
, f
.srid
)
204 return '%s(%%s,%s)' % (self
.from_text
, f
.srid
)
206 def _get_spatialite_func(self
, func
):
208 Helper routine for calling SpatiaLite functions and returning
211 cursor
= self
.connection
._cursor
()
214 cursor
.execute('SELECT %s' % func
)
215 row
= cursor
.fetchone()
217 # Responsibility of caller to perform error handling.
223 def geos_version(self
):
224 "Returns the version of GEOS used by SpatiaLite as a string."
225 return self
._get
_spatialite
_func
('geos_version()')
227 def proj4_version(self
):
228 "Returns the version of the PROJ.4 library used by SpatiaLite."
229 return self
._get
_spatialite
_func
('proj4_version()')
231 def spatialite_version(self
):
232 "Returns the SpatiaLite library version as a string."
233 return self
._get
_spatialite
_func
('spatialite_version()')
235 def spatialite_version_tuple(self
):
237 Returns the SpatiaLite version as a tuple (version string, major,
240 # Getting the SpatiaLite version.
242 version
= self
.spatialite_version()
243 except DatabaseError
:
244 # The `spatialite_version` function first appeared in version 2.3.1
245 # of SpatiaLite, so doing a fallback test for 2.3.0 (which is
246 # used by popular Debian/Ubuntu packages).
249 tmp
= self
._get
_spatialite
_func
("X(GeomFromText('POINT(1 1)'))")
250 if tmp
== 1.0: version
= '2.3.0'
251 except DatabaseError
:
253 # If no version string defined, then just re-raise the original
255 if version
is None: raise
257 m
= self
.version_regex
.match(version
)
259 major
= int(m
.group('major'))
260 minor1
= int(m
.group('minor1'))
261 minor2
= int(m
.group('minor2'))
263 raise Exception('Could not parse SpatiaLite version string: %s' % version
)
265 return (version
, major
, minor1
, minor2
)
267 def spatial_aggregate_sql(self
, agg
):
269 Returns the spatial aggregate SQL template and function for the
270 given Aggregate instance.
272 agg_name
= agg
.__class
__.__name
__
273 if not self
.check_aggregate_support(agg
):
274 raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name
)
275 agg_name
= agg_name
.lower()
276 if agg_name
== 'union': agg_name
+= 'agg'
277 sql_template
= self
.select
% '%(function)s(%(field)s)'
278 sql_function
= getattr(self
, agg_name
)
279 return sql_template
, sql_function
281 def spatial_lookup_sql(self
, lvalue
, lookup_type
, value
, field
, qn
):
283 Returns the SpatiaLite-specific SQL for the given lookup value
284 [a tuple of (alias, column, db_type)], lookup type, lookup
285 value, the model field, and the quoting function.
287 alias
, col
, db_type
= lvalue
289 # Getting the quoted field as `geo_col`.
290 geo_col
= '%s.%s' % (qn(alias
), qn(col
))
292 if lookup_type
in self
.geometry_functions
:
293 # See if a SpatiaLite geometry function matches the lookup type.
294 tmp
= self
.geometry_functions
[lookup_type
]
296 # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
298 if isinstance(tmp
, tuple):
299 # First element of tuple is the SpatiaLiteOperation instance, and the
300 # second element is either the type or a tuple of acceptable types
301 # that may passed in as further parameters for the lookup type.
304 # Ensuring that a tuple _value_ was passed in from the user
305 if not isinstance(value
, (tuple, list)):
306 raise ValueError('Tuple required for `%s` lookup type.' % lookup_type
)
308 # Geometry is first element of lookup tuple.
311 # Number of valid tuple parameters depends on the lookup type.
313 raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type
)
315 # Ensuring the argument type matches what we expect.
316 if not isinstance(value
[1], arg_type
):
317 raise ValueError('Argument type should be %s, got %s instead.' % (arg_type
, type(value
[1])))
319 # For lookup type `relate`, the op instance is not yet created (has
320 # to be instantiated here to check the pattern parameter).
321 if lookup_type
== 'relate':
323 elif lookup_type
in self
.distance_functions
:
328 # Calling the `as_sql` function on the operation instance.
329 return op
.as_sql(geo_col
, self
.get_geom_placeholder(field
, geom
))
330 elif lookup_type
== 'isnull':
331 # Handling 'isnull' lookup type
332 return "%s IS %sNULL" % (geo_col
, (not value
and 'NOT ' or ''))
334 raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type
))
336 # Routines for getting the OGC-compliant models.
337 def geometry_columns(self
):
338 from django
.contrib
.gis
.db
.backends
.spatialite
.models
import GeometryColumns
339 return GeometryColumns
341 def spatial_ref_sys(self
):
342 from django
.contrib
.gis
.db
.backends
.spatialite
.models
import SpatialRefSys