PageRenderTime 216ms CodeModel.GetById 70ms app.highlight 67ms RepoModel.GetById 74ms app.codeStats 0ms

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

https://code.google.com/p/mango-py/
Python | 343 lines | 292 code | 18 blank | 33 comment | 18 complexity | ec6ca6f22902eb2ecd83df1ad873f6e9 MD5 | raw file
  1import re
  2from decimal import Decimal
  3
  4from django.contrib.gis.db.backends.base import BaseSpatialOperations
  5from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
  6from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
  7from django.contrib.gis.geometry.backend import Geometry
  8from django.contrib.gis.measure import Distance
  9from django.core.exceptions import ImproperlyConfigured
 10from django.db.backends.sqlite3.base import DatabaseOperations
 11from django.db.utils import DatabaseError
 12
 13class SpatiaLiteOperator(SpatialOperation):
 14    "For SpatiaLite operators (e.g. `&&`, `~`)."
 15    def __init__(self, operator):
 16        super(SpatiaLiteOperator, self).__init__(operator=operator)
 17
 18class SpatiaLiteFunction(SpatialFunction):
 19    "For SpatiaLite function calls."
 20    def __init__(self, function, **kwargs):
 21        super(SpatiaLiteFunction, self).__init__(function, **kwargs)
 22
 23class SpatiaLiteFunctionParam(SpatiaLiteFunction):
 24    "For SpatiaLite functions that take another parameter."
 25    sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
 26
 27class SpatiaLiteDistance(SpatiaLiteFunction):
 28    "For SpatiaLite distance operations."
 29    dist_func = 'Distance'
 30    sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
 31
 32    def __init__(self, operator):
 33        super(SpatiaLiteDistance, self).__init__(self.dist_func,
 34                                                 operator=operator)
 35
 36class 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')
 43
 44# Valid distance types and substitutions
 45dtypes = (Decimal, Distance, float, int, long)
 46def get_dist_ops(operator):
 47    "Returns operations for regular distances; spherical distances are not currently supported."
 48    return (SpatiaLiteDistance(operator),)
 49
 50class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
 51    compiler_module = 'django.contrib.gis.db.backends.spatialite.compiler'
 52    name = 'spatialite'
 53    spatialite = True
 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')])
 56
 57    Adapter = SpatiaLiteAdapter
 58    Adaptor = Adapter # Backwards-compatibility alias.
 59
 60    area = 'Area'
 61    centroid = 'Centroid'
 62    contained = 'MbrWithin'
 63    difference = 'Difference'
 64    distance = 'Distance'
 65    envelope = 'Envelope'
 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'
 71    scale = 'ScaleCoords'
 72    svg = 'AsSVG'
 73    sym_difference = 'SymDifference'
 74    transform = 'Transform'
 75    translate = 'ShiftCoords'
 76    union = 'GUnion' # OpenGis defines Union, but this conflicts with an SQLite reserved keyword
 77    unionagg = 'GUnion'
 78
 79    from_text = 'GeomFromText'
 80    from_wkb = 'GeomFromWKB'
 81    select = 'AsText(%s)'
 82
 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'),
102        }
103
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),
109        }
110    geometry_functions.update(distance_functions)
111
112    def __init__(self, connection):
113        super(DatabaseOperations, self).__init__()
114        self.connection = connection
115
116        # Determine the version of the SpatiaLite library.
117        try:
118            vtup = self.spatialite_version_tuple()
119            version = vtup[1:]
120            if version < (2, 3, 0):
121                raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions '
122                                           '2.3.0 and above')
123            self.spatial_version = version
124        except ImproperlyConfigured:
125            raise
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))
131
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])
136
137    def check_aggregate_support(self, aggregate):
138        """
139        Checks if the given aggregate name is supported (that is, if it's
140        in `self.valid_aggregates`).
141        """
142        agg_name = aggregate.__class__.__name__
143        return agg_name in self.valid_aggregates
144
145    def convert_geom(self, wkt, geo_field):
146        """
147        Converts geometry WKT returned from a SpatiaLite aggregate.
148        """
149        if wkt:
150            return Geometry(wkt, geo_field.srid)
151        else:
152            return None
153
154    def geo_db_type(self, f):
155        """
156        Returns None because geometry columnas are added via the
157        `AddGeometryColumn` stored procedure on SpatiaLite.
158        """
159        return None
160
161    def get_distance(self, f, value, lookup_type):
162        """
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).
167        """
168        if not value:
169            return []
170        value = value[0]
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.')
177            else:
178                dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
179        else:
180            dist_param = value
181        return [dist_param]
182
183    def get_geom_placeholder(self, f, value):
184        """
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).
188        """
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)
194            else:
195                placeholder = '%s'
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]))
199        else:
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)
203            else:
204                return '%s(%%s,%s)' % (self.from_text, f.srid)
205
206    def _get_spatialite_func(self, func):
207        """
208        Helper routine for calling SpatiaLite functions and returning
209        their result.
210        """
211        cursor = self.connection._cursor()
212        try:
213            try:
214                cursor.execute('SELECT %s' % func)
215                row = cursor.fetchone()
216            except:
217                # Responsibility of caller to perform error handling.
218                raise
219        finally:
220            cursor.close()
221        return row[0]
222
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()')
226
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()')
230
231    def spatialite_version(self):
232        "Returns the SpatiaLite library version as a string."
233        return self._get_spatialite_func('spatialite_version()')
234
235    def spatialite_version_tuple(self):
236        """
237        Returns the SpatiaLite version as a tuple (version string, major,
238        minor, subminor).
239        """
240        # Getting the SpatiaLite version.
241        try:
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).
247            version = None
248            try:
249                tmp = self._get_spatialite_func("X(GeomFromText('POINT(1 1)'))")
250                if tmp == 1.0: version = '2.3.0'
251            except DatabaseError:
252                pass
253            # If no version string defined, then just re-raise the original
254            # exception.
255            if version is None: raise
256
257        m = self.version_regex.match(version)
258        if m:
259            major = int(m.group('major'))
260            minor1 = int(m.group('minor1'))
261            minor2 = int(m.group('minor2'))
262        else:
263            raise Exception('Could not parse SpatiaLite version string: %s' % version)
264
265        return (version, major, minor1, minor2)
266
267    def spatial_aggregate_sql(self, agg):
268        """
269        Returns the spatial aggregate SQL template and function for the
270        given Aggregate instance.
271        """
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
280
281    def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
282        """
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.
286        """
287        alias, col, db_type = lvalue
288
289        # Getting the quoted field as `geo_col`.
290        geo_col = '%s.%s' % (qn(alias), qn(col))
291
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]
295
296            # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
297            # distance lookups.
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.
302                op, arg_type = tmp
303
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)
307
308                # Geometry is first element of lookup tuple.
309                geom = value[0]
310
311                # Number of valid tuple parameters depends on the lookup type.
312                if len(value) != 2:
313                    raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
314
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])))
318
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':
322                    op = op(value[1])
323                elif lookup_type in self.distance_functions:
324                    op = op[0]
325            else:
326                op = tmp
327                geom = value
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 ''))
333
334        raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
335
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
340
341    def spatial_ref_sys(self):
342        from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys
343        return SpatialRefSys