123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import re
-
- from django.conf import settings
- from django.contrib.gis.db.backends.base.operations import (
- BaseSpatialOperations,
- )
- from django.contrib.gis.db.backends.utils import SpatialOperator
- from django.contrib.gis.db.models import GeometryField, RasterField
- from django.contrib.gis.gdal import GDALRaster
- from django.contrib.gis.geos.geometry import GEOSGeometryBase
- from django.contrib.gis.geos.prototypes.io import wkb_r
- from django.contrib.gis.measure import Distance
- from django.core.exceptions import ImproperlyConfigured
- from django.db.backends.postgresql.operations import DatabaseOperations
- from django.db.models import Func, Value
- from django.db.utils import NotSupportedError, ProgrammingError
- from django.utils.functional import cached_property
- from django.utils.version import get_version_tuple
-
- from .adapter import PostGISAdapter
- from .models import PostGISGeometryColumns, PostGISSpatialRefSys
- from .pgraster import from_pgraster
-
- # Identifier to mark raster lookups as bilateral.
- BILATERAL = 'bilateral'
-
-
- class PostGISOperator(SpatialOperator):
- def __init__(self, geography=False, raster=False, **kwargs):
- # Only a subset of the operators and functions are available for the
- # geography type.
- self.geography = geography
- # Only a subset of the operators and functions are available for the
- # raster type. Lookups that don't support raster will be converted to
- # polygons. If the raster argument is set to BILATERAL, then the
- # operator cannot handle mixed geom-raster lookups.
- self.raster = raster
- super().__init__(**kwargs)
-
- def as_sql(self, connection, lookup, template_params, *args):
- if lookup.lhs.output_field.geography and not self.geography:
- raise ValueError('PostGIS geography does not support the "%s" '
- 'function/operator.' % (self.func or self.op,))
-
- template_params = self.check_raster(lookup, template_params)
- return super().as_sql(connection, lookup, template_params, *args)
-
- def check_raster(self, lookup, template_params):
- spheroid = lookup.rhs_params and lookup.rhs_params[-1] == 'spheroid'
-
- # Check which input is a raster.
- lhs_is_raster = lookup.lhs.field.geom_type == 'RASTER'
- rhs_is_raster = isinstance(lookup.rhs, GDALRaster)
-
- # Look for band indices and inject them if provided.
- if lookup.band_lhs is not None and lhs_is_raster:
- if not self.func:
- raise ValueError('Band indices are not allowed for this operator, it works on bbox only.')
- template_params['lhs'] = '%s, %s' % (template_params['lhs'], lookup.band_lhs)
-
- if lookup.band_rhs is not None and rhs_is_raster:
- if not self.func:
- raise ValueError('Band indices are not allowed for this operator, it works on bbox only.')
- template_params['rhs'] = '%s, %s' % (template_params['rhs'], lookup.band_rhs)
-
- # Convert rasters to polygons if necessary.
- if not self.raster or spheroid:
- # Operators without raster support.
- if lhs_is_raster:
- template_params['lhs'] = 'ST_Polygon(%s)' % template_params['lhs']
- if rhs_is_raster:
- template_params['rhs'] = 'ST_Polygon(%s)' % template_params['rhs']
- elif self.raster == BILATERAL:
- # Operators with raster support but don't support mixed (rast-geom)
- # lookups.
- if lhs_is_raster and not rhs_is_raster:
- template_params['lhs'] = 'ST_Polygon(%s)' % template_params['lhs']
- elif rhs_is_raster and not lhs_is_raster:
- template_params['rhs'] = 'ST_Polygon(%s)' % template_params['rhs']
-
- return template_params
-
-
- class ST_Polygon(Func):
- function = 'ST_Polygon'
-
- def __init__(self, expr):
- super().__init__(expr)
- expr = self.source_expressions[0]
- if isinstance(expr, Value) and not expr._output_field_or_none:
- self.source_expressions[0] = Value(expr.value, output_field=RasterField(srid=expr.value.srid))
-
- @cached_property
- def output_field(self):
- return GeometryField(srid=self.source_expressions[0].field.srid)
-
-
- class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
- name = 'postgis'
- postgis = True
- geography = True
- geom_func_prefix = 'ST_'
-
- Adapter = PostGISAdapter
-
- collect = geom_func_prefix + 'Collect'
- extent = geom_func_prefix + 'Extent'
- extent3d = geom_func_prefix + '3DExtent'
- length3d = geom_func_prefix + '3DLength'
- makeline = geom_func_prefix + 'MakeLine'
- perimeter3d = geom_func_prefix + '3DPerimeter'
- unionagg = geom_func_prefix + 'Union'
-
- gis_operators = {
- 'bbcontains': PostGISOperator(op='~', raster=True),
- 'bboverlaps': PostGISOperator(op='&&', geography=True, raster=True),
- 'contained': PostGISOperator(op='@', raster=True),
- 'overlaps_left': PostGISOperator(op='&<', raster=BILATERAL),
- 'overlaps_right': PostGISOperator(op='&>', raster=BILATERAL),
- 'overlaps_below': PostGISOperator(op='&<|'),
- 'overlaps_above': PostGISOperator(op='|&>'),
- 'left': PostGISOperator(op='<<'),
- 'right': PostGISOperator(op='>>'),
- 'strictly_below': PostGISOperator(op='<<|'),
- 'strictly_above': PostGISOperator(op='|>>'),
- 'same_as': PostGISOperator(op='~=', raster=BILATERAL),
- 'exact': PostGISOperator(op='~=', raster=BILATERAL), # alias of same_as
- 'contains': PostGISOperator(func='ST_Contains', raster=BILATERAL),
- 'contains_properly': PostGISOperator(func='ST_ContainsProperly', raster=BILATERAL),
- 'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True, raster=BILATERAL),
- 'covers': PostGISOperator(func='ST_Covers', geography=True, raster=BILATERAL),
- 'crosses': PostGISOperator(func='ST_Crosses'),
- 'disjoint': PostGISOperator(func='ST_Disjoint', raster=BILATERAL),
- 'equals': PostGISOperator(func='ST_Equals'),
- 'intersects': PostGISOperator(func='ST_Intersects', geography=True, raster=BILATERAL),
- 'overlaps': PostGISOperator(func='ST_Overlaps', raster=BILATERAL),
- 'relate': PostGISOperator(func='ST_Relate'),
- 'touches': PostGISOperator(func='ST_Touches', raster=BILATERAL),
- 'within': PostGISOperator(func='ST_Within', raster=BILATERAL),
- 'dwithin': PostGISOperator(func='ST_DWithin', geography=True, raster=BILATERAL),
- }
-
- unsupported_functions = set()
-
- select = '%s::bytea'
- select_extent = None
-
- @cached_property
- def function_names(self):
- function_names = {
- 'BoundingCircle': 'ST_MinimumBoundingCircle',
- 'NumPoints': 'ST_NPoints',
- }
- if self.spatial_version < (2, 2, 0):
- function_names.update({
- 'DistanceSphere': 'ST_distance_sphere',
- 'DistanceSpheroid': 'ST_distance_spheroid',
- 'LengthSpheroid': 'ST_length_spheroid',
- 'MemSize': 'ST_mem_size',
- })
- if self.spatial_version < (2, 4, 0):
- function_names['ForcePolygonCW'] = 'ST_ForceRHR'
- return function_names
-
- @cached_property
- def spatial_version(self):
- """Determine the version of the PostGIS library."""
- # Trying to get the PostGIS version because the function
- # signatures will depend on the version used. The cost
- # here is a database query to determine the version, which
- # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
- # comprising user-supplied values for the major, minor, and
- # subminor revision of PostGIS.
- if hasattr(settings, 'POSTGIS_VERSION'):
- version = settings.POSTGIS_VERSION
- else:
- # Run a basic query to check the status of the connection so we're
- # sure we only raise the error below if the problem comes from
- # PostGIS and not from PostgreSQL itself (see #24862).
- self._get_postgis_func('version')
-
- try:
- vtup = self.postgis_version_tuple()
- except ProgrammingError:
- raise ImproperlyConfigured(
- 'Cannot determine PostGIS version for database "%s" '
- 'using command "SELECT postgis_lib_version()". '
- 'GeoDjango requires at least PostGIS version 2.1. '
- 'Was the database created from a spatial database '
- 'template?' % self.connection.settings_dict['NAME']
- )
- version = vtup[1:]
- return version
-
- def convert_extent(self, box):
- """
- Return a 4-tuple extent for the `Extent` aggregate by converting
- the bounding box text returned by PostGIS (`box` argument), for
- example: "BOX(-90.0 30.0, -85.0 40.0)".
- """
- if box is None:
- return None
- ll, ur = box[4:-1].split(',')
- xmin, ymin = map(float, ll.split())
- xmax, ymax = map(float, ur.split())
- return (xmin, ymin, xmax, ymax)
-
- def convert_extent3d(self, box3d):
- """
- Return a 6-tuple extent for the `Extent3D` aggregate by converting
- the 3d bounding-box text returned by PostGIS (`box3d` argument), for
- example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
- """
- if box3d is None:
- return None
- ll, ur = box3d[6:-1].split(',')
- xmin, ymin, zmin = map(float, ll.split())
- xmax, ymax, zmax = map(float, ur.split())
- return (xmin, ymin, zmin, xmax, ymax, zmax)
-
- def geo_db_type(self, f):
- """
- Return the database field type for the given spatial field.
- """
- if f.geom_type == 'RASTER':
- return 'raster'
-
- # Type-based geometries.
- # TODO: Support 'M' extension.
- if f.dim == 3:
- geom_type = f.geom_type + 'Z'
- else:
- geom_type = f.geom_type
- if f.geography:
- if f.srid != 4326:
- raise NotSupportedError('PostGIS only supports geography columns with an SRID of 4326.')
-
- return 'geography(%s,%d)' % (geom_type, f.srid)
- else:
- return 'geometry(%s,%d)' % (geom_type, f.srid)
-
- def get_distance(self, f, dist_val, lookup_type):
- """
- Retrieve the distance parameters for the given geometry field,
- distance lookup value, and the distance lookup type.
-
- This is the most complex implementation of the spatial backends due to
- what is supported on geodetic geometry columns vs. what's available on
- projected geometry columns. In addition, it has to take into account
- the geography column type.
- """
- # Getting the distance parameter
- value = dist_val[0]
-
- # Shorthand boolean flags.
- geodetic = f.geodetic(self.connection)
- geography = f.geography
-
- if isinstance(value, Distance):
- if geography:
- dist_param = value.m
- elif geodetic:
- if lookup_type == 'dwithin':
- raise ValueError('Only numeric values of degree units are '
- 'allowed on geographic DWithin queries.')
- dist_param = value.m
- else:
- dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
- else:
- # Assuming the distance is in the units of the field.
- dist_param = value
-
- return [dist_param]
-
- def get_geom_placeholder(self, f, value, compiler):
- """
- Provide a proper substitution value for Geometries or rasters that are
- not in the SRID of the field. Specifically, this routine will
- substitute in the ST_Transform() function call.
- """
- transform_func = self.spatial_function_name('Transform')
- if hasattr(value, 'as_sql'):
- if value.field.srid == f.srid:
- placeholder = '%s'
- else:
- placeholder = '%s(%%s, %s)' % (transform_func, f.srid)
- return placeholder
-
- # Get the srid for this object
- if value is None:
- value_srid = None
- else:
- value_srid = value.srid
-
- # Adding Transform() to the SQL placeholder if the value srid
- # is not equal to the field srid.
- if value_srid is None or value_srid == f.srid:
- placeholder = '%s'
- else:
- placeholder = '%s(%%s, %s)' % (transform_func, f.srid)
-
- return placeholder
-
- def _get_postgis_func(self, func):
- """
- Helper routine for calling PostGIS functions and returning their result.
- """
- # Close out the connection. See #9437.
- with self.connection.temporary_connection() as cursor:
- cursor.execute('SELECT %s()' % func)
- return cursor.fetchone()[0]
-
- def postgis_geos_version(self):
- "Return the version of the GEOS library used with PostGIS."
- return self._get_postgis_func('postgis_geos_version')
-
- def postgis_lib_version(self):
- "Return the version number of the PostGIS library used with PostgreSQL."
- return self._get_postgis_func('postgis_lib_version')
-
- def postgis_proj_version(self):
- "Return the version of the PROJ.4 library used with PostGIS."
- return self._get_postgis_func('postgis_proj_version')
-
- def postgis_version(self):
- "Return PostGIS version number and compile-time options."
- return self._get_postgis_func('postgis_version')
-
- def postgis_full_version(self):
- "Return PostGIS version number and compile-time options."
- return self._get_postgis_func('postgis_full_version')
-
- def postgis_version_tuple(self):
- """
- Return the PostGIS version as a tuple (version string, major,
- minor, subminor).
- """
- version = self.postgis_lib_version()
- return (version,) + get_version_tuple(version)
-
- def proj_version_tuple(self):
- """
- Return the version of PROJ.4 used by PostGIS as a tuple of the
- major, minor, and subminor release numbers.
- """
- proj_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)')
- proj_ver_str = self.postgis_proj_version()
- m = proj_regex.search(proj_ver_str)
- if m:
- return tuple(map(int, [m.group(1), m.group(2), m.group(3)]))
- else:
- raise Exception('Could not determine PROJ.4 version from PostGIS.')
-
- def spatial_aggregate_name(self, agg_name):
- if agg_name == 'Extent3D':
- return self.extent3d
- else:
- return self.geom_func_prefix + agg_name
-
- # Routines for getting the OGC-compliant models.
- def geometry_columns(self):
- return PostGISGeometryColumns
-
- def spatial_ref_sys(self):
- return PostGISSpatialRefSys
-
- def parse_raster(self, value):
- """Convert a PostGIS HEX String into a dict readable by GDALRaster."""
- return from_pgraster(value)
-
- def distance_expr_for_lookup(self, lhs, rhs, **kwargs):
- return super().distance_expr_for_lookup(
- self._normalize_distance_lookup_arg(lhs),
- self._normalize_distance_lookup_arg(rhs),
- **kwargs
- )
-
- @staticmethod
- def _normalize_distance_lookup_arg(arg):
- is_raster = (
- arg.field.geom_type == 'RASTER'
- if hasattr(arg, 'field') else
- isinstance(arg, GDALRaster)
- )
- return ST_Polygon(arg) if is_raster else arg
-
- def get_geometry_converter(self, expression):
- read = wkb_r().read
- geom_class = expression.output_field.geom_class
-
- def converter(value, expression, connection):
- return None if value is None else GEOSGeometryBase(read(value), geom_class)
- return converter
-
- def get_area_att_for_field(self, field):
- return 'sq_m'
|