web/lib/django/contrib/gis/db/backends/spatialite/operations.py
changeset 38 77b6da96e6f1
parent 29 cc9b7e14412b
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 import re
       
     2 from decimal import Decimal
       
     3 
       
     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
       
    12 
       
    13 class SpatiaLiteOperator(SpatialOperation):
       
    14     "For SpatiaLite operators (e.g. `&&`, `~`)."
       
    15     def __init__(self, operator):
       
    16         super(SpatiaLiteOperator, self).__init__(operator=operator)
       
    17 
       
    18 class SpatiaLiteFunction(SpatialFunction):
       
    19     "For SpatiaLite function calls."
       
    20     def __init__(self, function, **kwargs):
       
    21         super(SpatiaLiteFunction, self).__init__(function, **kwargs)
       
    22 
       
    23 class SpatiaLiteFunctionParam(SpatiaLiteFunction):
       
    24     "For SpatiaLite functions that take another parameter."
       
    25     sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
       
    26 
       
    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'
       
    31 
       
    32     def __init__(self, operator):
       
    33         super(SpatiaLiteDistance, self).__init__(self.dist_func,
       
    34                                                  operator=operator)
       
    35 
       
    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')
       
    43 
       
    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),)
       
    49 
       
    50 class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
       
    51     compiler_module = 'django.contrib.gis.db.models.sql.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