| 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 |
| from django.utils import six |
| from django.utils.functional import cached_property |
| |
| |
| 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(<geom>, <pattern>) 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) + six.integer_types |
| 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<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\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, six.string_types), |
| # Returns 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__(connection) |
| |
| # 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 confirm_spatial_components_versions(self): |
| # 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 as 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)) |
| |
| if version >= (2, 4, 0): |
| # Spatialite 2.4.0-RC4 added AsGML and AsKML, however both |
| # RC2 (shipped in popular Debian/Ubuntu packages) and RC4 |
| # report version as '2.4.0', so we fall back to feature detection |
| try: |
| self._get_spatialite_func("AsGML(GeomFromText('POINT(1 1)'))") |
| self.gml = 'AsGML' |
| self.kml = 'AsKML' |
| except DatabaseError: |
| # we are using < 2.4.0-RC4 |
| pass |
| |
| @cached_property |
| def geojson(self): |
| return 'AsGeoJSON' if self.spatialite_version_tuple()[1:] >= (3, 0, 0) else None |
| |
| 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 % self.get_expression_column(value) |
| 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 |