diff -r b758351d191f -r cc9b7e14412b web/lib/django/contrib/gis/db/backends/spatialite/operations.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/lib/django/contrib/gis/db/backends/spatialite/operations.py Tue May 25 02:43:45 2010 +0200 @@ -0,0 +1,343 @@ +import re +from decimal import Decimal + +from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction +from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter +from django.contrib.gis.geometry.backend import Geometry +from django.contrib.gis.measure import Distance +from django.core.exceptions import ImproperlyConfigured +from django.db.backends.sqlite3.base import DatabaseOperations +from django.db.utils import DatabaseError + +class SpatiaLiteOperator(SpatialOperation): + "For SpatiaLite operators (e.g. `&&`, `~`)." + def __init__(self, operator): + super(SpatiaLiteOperator, self).__init__(operator=operator) + +class SpatiaLiteFunction(SpatialFunction): + "For SpatiaLite function calls." + def __init__(self, function, **kwargs): + super(SpatiaLiteFunction, self).__init__(function, **kwargs) + +class SpatiaLiteFunctionParam(SpatiaLiteFunction): + "For SpatiaLite functions that take another parameter." + sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)' + +class SpatiaLiteDistance(SpatiaLiteFunction): + "For SpatiaLite distance operations." + dist_func = 'Distance' + sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s' + + def __init__(self, operator): + super(SpatiaLiteDistance, self).__init__(self.dist_func, + operator=operator) + +class SpatiaLiteRelate(SpatiaLiteFunctionParam): + "For SpatiaLite Relate(, ) calls." + pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, pattern): + if not self.pattern_regex.match(pattern): + raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) + super(SpatiaLiteRelate, self).__init__('Relate') + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +def get_dist_ops(operator): + "Returns operations for regular distances; spherical distances are not currently supported." + return (SpatiaLiteDistance(operator),) + +class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): + compiler_module = 'django.contrib.gis.db.models.sql.compiler' + name = 'spatialite' + spatialite = True + version_regex = re.compile(r'^(?P\d)\.(?P\d)\.(?P\d+)') + valid_aggregates = dict([(k, None) for k in ('Extent', 'Union')]) + + Adapter = SpatiaLiteAdapter + Adaptor = Adapter # Backwards-compatibility alias. + + area = 'Area' + centroid = 'Centroid' + contained = 'MbrWithin' + difference = 'Difference' + distance = 'Distance' + envelope = 'Envelope' + intersection = 'Intersection' + length = 'GLength' # OpenGis defines Length, but this conflicts with an SQLite reserved keyword + num_geom = 'NumGeometries' + num_points = 'NumPoints' + point_on_surface = 'PointOnSurface' + scale = 'ScaleCoords' + svg = 'AsSVG' + sym_difference = 'SymDifference' + transform = 'Transform' + translate = 'ShiftCoords' + union = 'GUnion' # OpenGis defines Union, but this conflicts with an SQLite reserved keyword + unionagg = 'GUnion' + + from_text = 'GeomFromText' + from_wkb = 'GeomFromWKB' + select = 'AsText(%s)' + + geometry_functions = { + 'equals' : SpatiaLiteFunction('Equals'), + 'disjoint' : SpatiaLiteFunction('Disjoint'), + 'touches' : SpatiaLiteFunction('Touches'), + 'crosses' : SpatiaLiteFunction('Crosses'), + 'within' : SpatiaLiteFunction('Within'), + 'overlaps' : SpatiaLiteFunction('Overlaps'), + 'contains' : SpatiaLiteFunction('Contains'), + 'intersects' : SpatiaLiteFunction('Intersects'), + 'relate' : (SpatiaLiteRelate, basestring), + # Retruns true if B's bounding box completely contains A's bounding box. + 'contained' : SpatiaLiteFunction('MbrWithin'), + # Returns true if A's bounding box completely contains B's bounding box. + 'bbcontains' : SpatiaLiteFunction('MbrContains'), + # Returns true if A's bounding box overlaps B's bounding box. + 'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'), + # These are implemented here as synonyms for Equals + 'same_as' : SpatiaLiteFunction('Equals'), + 'exact' : SpatiaLiteFunction('Equals'), + } + + distance_functions = { + 'distance_gt' : (get_dist_ops('>'), dtypes), + 'distance_gte' : (get_dist_ops('>='), dtypes), + 'distance_lt' : (get_dist_ops('<'), dtypes), + 'distance_lte' : (get_dist_ops('<='), dtypes), + } + geometry_functions.update(distance_functions) + + def __init__(self, connection): + super(DatabaseOperations, self).__init__() + self.connection = connection + + # Determine the version of the SpatiaLite library. + try: + vtup = self.spatialite_version_tuple() + version = vtup[1:] + if version < (2, 3, 0): + raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions ' + '2.3.0 and above') + self.spatial_version = version + except ImproperlyConfigured: + raise + except Exception, msg: + raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" ' + 'database (error was "%s"). Was the SpatiaLite initialization ' + 'SQL loaded on this database?' % + (self.connection.settings_dict['NAME'], msg)) + + # Creating the GIS terms dictionary. + gis_terms = ['isnull'] + gis_terms += self.geometry_functions.keys() + self.gis_terms = dict([(term, None) for term in gis_terms]) + + def check_aggregate_support(self, aggregate): + """ + Checks if the given aggregate name is supported (that is, if it's + in `self.valid_aggregates`). + """ + agg_name = aggregate.__class__.__name__ + return agg_name in self.valid_aggregates + + def convert_geom(self, wkt, geo_field): + """ + Converts geometry WKT returned from a SpatiaLite aggregate. + """ + if wkt: + return Geometry(wkt, geo_field.srid) + else: + return None + + def geo_db_type(self, f): + """ + Returns None because geometry columnas are added via the + `AddGeometryColumn` stored procedure on SpatiaLite. + """ + return None + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. SpatiaLite only supports regular + cartesian-based queries (no spheroid/sphere calculations for point + geometries like PostGIS). + """ + if not value: + return [] + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + raise ValueError('SpatiaLite does not support distance queries on ' + 'geometry fields with a geodetic coordinate system. ' + 'Distance objects; use a numeric value of your ' + 'distance in degrees instead.') + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + return [dist_param] + + def get_geom_placeholder(self, f, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + Transform() and GeomFromText() function call(s). + """ + def transform_value(value, srid): + return not (value is None or value.srid == srid) + if hasattr(value, 'expression'): + if transform_value(value, f.srid): + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) + else: + placeholder = '%s' + # No geometry value used for F expression, substitue in + # the column name instead. + return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + else: + if transform_value(value, f.srid): + # Adding Transform() to the SQL placeholder. + return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, f.srid) + else: + return '%s(%%s,%s)' % (self.from_text, f.srid) + + def _get_spatialite_func(self, func): + """ + Helper routine for calling SpatiaLite functions and returning + their result. + """ + cursor = self.connection._cursor() + try: + try: + cursor.execute('SELECT %s' % func) + row = cursor.fetchone() + except: + # Responsibility of caller to perform error handling. + raise + finally: + cursor.close() + return row[0] + + def geos_version(self): + "Returns the version of GEOS used by SpatiaLite as a string." + return self._get_spatialite_func('geos_version()') + + def proj4_version(self): + "Returns the version of the PROJ.4 library used by SpatiaLite." + return self._get_spatialite_func('proj4_version()') + + def spatialite_version(self): + "Returns the SpatiaLite library version as a string." + return self._get_spatialite_func('spatialite_version()') + + def spatialite_version_tuple(self): + """ + Returns the SpatiaLite version as a tuple (version string, major, + minor, subminor). + """ + # Getting the SpatiaLite version. + try: + version = self.spatialite_version() + except DatabaseError: + # The `spatialite_version` function first appeared in version 2.3.1 + # of SpatiaLite, so doing a fallback test for 2.3.0 (which is + # used by popular Debian/Ubuntu packages). + version = None + try: + tmp = self._get_spatialite_func("X(GeomFromText('POINT(1 1)'))") + if tmp == 1.0: version = '2.3.0' + except DatabaseError: + pass + # If no version string defined, then just re-raise the original + # exception. + if version is None: raise + + m = self.version_regex.match(version) + if m: + major = int(m.group('major')) + minor1 = int(m.group('minor1')) + minor2 = int(m.group('minor2')) + else: + raise Exception('Could not parse SpatiaLite version string: %s' % version) + + return (version, major, minor1, minor2) + + def spatial_aggregate_sql(self, agg): + """ + Returns the spatial aggregate SQL template and function for the + given Aggregate instance. + """ + agg_name = agg.__class__.__name__ + if not self.check_aggregate_support(agg): + raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name) + agg_name = agg_name.lower() + if agg_name == 'union': agg_name += 'agg' + sql_template = self.select % '%(function)s(%(field)s)' + sql_function = getattr(self, agg_name) + return sql_template, sql_function + + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): + """ + Returns the SpatiaLite-specific SQL for the given lookup value + [a tuple of (alias, column, db_type)], lookup type, lookup + value, the model field, and the quoting function. + """ + alias, col, db_type = lvalue + + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(alias), qn(col)) + + if lookup_type in self.geometry_functions: + # See if a SpatiaLite geometry function matches the lookup type. + tmp = self.geometry_functions[lookup_type] + + # Lookup types that are tuples take tuple arguments, e.g., 'relate' and + # distance lookups. + if isinstance(tmp, tuple): + # First element of tuple is the SpatiaLiteOperation instance, and the + # second element is either the type or a tuple of acceptable types + # that may passed in as further parameters for the lookup type. + op, arg_type = tmp + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(value, (tuple, list)): + raise ValueError('Tuple required for `%s` lookup type.' % lookup_type) + + # Geometry is first element of lookup tuple. + geom = value[0] + + # Number of valid tuple parameters depends on the lookup type. + if len(value) != 2: + raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(value[1], arg_type): + raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) + + # For lookup type `relate`, the op instance is not yet created (has + # to be instantiated here to check the pattern parameter). + if lookup_type == 'relate': + op = op(value[1]) + elif lookup_type in self.distance_functions: + op = op[0] + else: + op = tmp + geom = value + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + from django.contrib.gis.db.backends.spatialite.models import GeometryColumns + return GeometryColumns + + def spatial_ref_sys(self): + from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys + return SpatialRefSys