PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://gitlab.com/Guy1394/django
Python | 382 lines | 346 code | 19 blank | 17 comment | 17 complexity | b750fa4a2cbb5bbfc317c82dfba3e991 MD5 | raw file
  1. import re
  2. from django.conf import settings
  3. from django.contrib.gis.db.backends.base.operations import \
  4. BaseSpatialOperations
  5. from django.contrib.gis.db.backends.utils import SpatialOperator
  6. from django.contrib.gis.geometry.backend import Geometry
  7. from django.contrib.gis.measure import Distance
  8. from django.core.exceptions import ImproperlyConfigured
  9. from django.db.backends.postgresql.operations import DatabaseOperations
  10. from django.db.utils import ProgrammingError
  11. from django.utils.functional import cached_property
  12. from .adapter import PostGISAdapter
  13. from .models import PostGISGeometryColumns, PostGISSpatialRefSys
  14. from .pgraster import from_pgraster, get_pgraster_srid, to_pgraster
  15. class PostGISOperator(SpatialOperator):
  16. def __init__(self, geography=False, **kwargs):
  17. # Only a subset of the operators and functions are available
  18. # for the geography type.
  19. self.geography = geography
  20. super(PostGISOperator, self).__init__(**kwargs)
  21. def as_sql(self, connection, lookup, *args):
  22. if lookup.lhs.output_field.geography and not self.geography:
  23. raise ValueError('PostGIS geography does not support the "%s" '
  24. 'function/operator.' % (self.func or self.op,))
  25. return super(PostGISOperator, self).as_sql(connection, lookup, *args)
  26. class PostGISDistanceOperator(PostGISOperator):
  27. sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s'
  28. def as_sql(self, connection, lookup, template_params, sql_params):
  29. if not lookup.lhs.output_field.geography and lookup.lhs.output_field.geodetic(connection):
  30. sql_template = self.sql_template
  31. if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid':
  32. template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'})
  33. sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s) %(op)s %(value)s'
  34. # Using distance_spheroid requires the spheroid of the field as
  35. # a parameter.
  36. sql_params.insert(1, lookup.lhs.output_field._spheroid)
  37. else:
  38. template_params.update({'op': self.op, 'func': 'ST_Distance_Sphere'})
  39. return sql_template % template_params, sql_params
  40. return super(PostGISDistanceOperator, self).as_sql(connection, lookup, template_params, sql_params)
  41. class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
  42. name = 'postgis'
  43. postgis = True
  44. geography = True
  45. geom_func_prefix = 'ST_'
  46. version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
  47. Adapter = PostGISAdapter
  48. gis_operators = {
  49. 'bbcontains': PostGISOperator(op='~'),
  50. 'bboverlaps': PostGISOperator(op='&&', geography=True),
  51. 'contained': PostGISOperator(op='@'),
  52. 'contains': PostGISOperator(func='ST_Contains'),
  53. 'overlaps_left': PostGISOperator(op='&<'),
  54. 'overlaps_right': PostGISOperator(op='&>'),
  55. 'overlaps_below': PostGISOperator(op='&<|'),
  56. 'overlaps_above': PostGISOperator(op='|&>'),
  57. 'left': PostGISOperator(op='<<'),
  58. 'right': PostGISOperator(op='>>'),
  59. 'strictly_below': PostGISOperator(op='<<|'),
  60. 'strictly_above': PostGISOperator(op='|>>'),
  61. 'same_as': PostGISOperator(op='~='),
  62. 'exact': PostGISOperator(op='~='), # alias of same_as
  63. 'contains_properly': PostGISOperator(func='ST_ContainsProperly'),
  64. 'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True),
  65. 'covers': PostGISOperator(func='ST_Covers', geography=True),
  66. 'crosses': PostGISOperator(func='ST_Crosses'),
  67. 'disjoint': PostGISOperator(func='ST_Disjoint'),
  68. 'equals': PostGISOperator(func='ST_Equals'),
  69. 'intersects': PostGISOperator(func='ST_Intersects', geography=True),
  70. 'overlaps': PostGISOperator(func='ST_Overlaps'),
  71. 'relate': PostGISOperator(func='ST_Relate'),
  72. 'touches': PostGISOperator(func='ST_Touches'),
  73. 'within': PostGISOperator(func='ST_Within'),
  74. 'dwithin': PostGISOperator(func='ST_DWithin', geography=True),
  75. 'distance_gt': PostGISDistanceOperator(func='ST_Distance', op='>', geography=True),
  76. 'distance_gte': PostGISDistanceOperator(func='ST_Distance', op='>=', geography=True),
  77. 'distance_lt': PostGISDistanceOperator(func='ST_Distance', op='<', geography=True),
  78. 'distance_lte': PostGISDistanceOperator(func='ST_Distance', op='<=', geography=True),
  79. }
  80. unsupported_functions = set()
  81. function_names = {
  82. 'BoundingCircle': 'ST_MinimumBoundingCircle',
  83. 'MemSize': 'ST_Mem_Size',
  84. 'NumPoints': 'ST_NPoints',
  85. }
  86. def __init__(self, connection):
  87. super(PostGISOperations, self).__init__(connection)
  88. prefix = self.geom_func_prefix
  89. self.area = prefix + 'Area'
  90. self.bounding_circle = prefix + 'MinimumBoundingCircle'
  91. self.centroid = prefix + 'Centroid'
  92. self.collect = prefix + 'Collect'
  93. self.difference = prefix + 'Difference'
  94. self.distance = prefix + 'Distance'
  95. self.distance_sphere = prefix + 'distance_sphere'
  96. self.distance_spheroid = prefix + 'distance_spheroid'
  97. self.envelope = prefix + 'Envelope'
  98. self.extent = prefix + 'Extent'
  99. self.extent3d = prefix + '3DExtent'
  100. self.force_rhr = prefix + 'ForceRHR'
  101. self.geohash = prefix + 'GeoHash'
  102. self.geojson = prefix + 'AsGeoJson'
  103. self.gml = prefix + 'AsGML'
  104. self.intersection = prefix + 'Intersection'
  105. self.kml = prefix + 'AsKML'
  106. self.length = prefix + 'Length'
  107. self.length3d = prefix + '3DLength'
  108. self.length_spheroid = prefix + 'length_spheroid'
  109. self.makeline = prefix + 'MakeLine'
  110. self.mem_size = prefix + 'mem_size'
  111. self.num_geom = prefix + 'NumGeometries'
  112. self.num_points = prefix + 'npoints'
  113. self.perimeter = prefix + 'Perimeter'
  114. self.perimeter3d = prefix + '3DPerimeter'
  115. self.point_on_surface = prefix + 'PointOnSurface'
  116. self.polygonize = prefix + 'Polygonize'
  117. self.reverse = prefix + 'Reverse'
  118. self.scale = prefix + 'Scale'
  119. self.snap_to_grid = prefix + 'SnapToGrid'
  120. self.svg = prefix + 'AsSVG'
  121. self.sym_difference = prefix + 'SymDifference'
  122. self.transform = prefix + 'Transform'
  123. self.translate = prefix + 'Translate'
  124. self.union = prefix + 'Union'
  125. self.unionagg = prefix + 'Union'
  126. @cached_property
  127. def spatial_version(self):
  128. """Determine the version of the PostGIS library."""
  129. # Trying to get the PostGIS version because the function
  130. # signatures will depend on the version used. The cost
  131. # here is a database query to determine the version, which
  132. # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
  133. # comprising user-supplied values for the major, minor, and
  134. # subminor revision of PostGIS.
  135. if hasattr(settings, 'POSTGIS_VERSION'):
  136. version = settings.POSTGIS_VERSION
  137. else:
  138. # Run a basic query to check the status of the connection so we're
  139. # sure we only raise the error below if the problem comes from
  140. # PostGIS and not from PostgreSQL itself (see #24862).
  141. self._get_postgis_func('version')
  142. try:
  143. vtup = self.postgis_version_tuple()
  144. except ProgrammingError:
  145. raise ImproperlyConfigured(
  146. 'Cannot determine PostGIS version for database "%s" '
  147. 'using command "SELECT postgis_lib_version()". '
  148. 'GeoDjango requires at least PostGIS version 2.0. '
  149. 'Was the database created from a spatial database '
  150. 'template?' % self.connection.settings_dict['NAME']
  151. )
  152. version = vtup[1:]
  153. return version
  154. def convert_extent(self, box, srid):
  155. """
  156. Returns a 4-tuple extent for the `Extent` aggregate by converting
  157. the bounding box text returned by PostGIS (`box` argument), for
  158. example: "BOX(-90.0 30.0, -85.0 40.0)".
  159. """
  160. if box is None:
  161. return None
  162. ll, ur = box[4:-1].split(',')
  163. xmin, ymin = map(float, ll.split())
  164. xmax, ymax = map(float, ur.split())
  165. return (xmin, ymin, xmax, ymax)
  166. def convert_extent3d(self, box3d, srid):
  167. """
  168. Returns a 6-tuple extent for the `Extent3D` aggregate by converting
  169. the 3d bounding-box text returned by PostGIS (`box3d` argument), for
  170. example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
  171. """
  172. if box3d is None:
  173. return None
  174. ll, ur = box3d[6:-1].split(',')
  175. xmin, ymin, zmin = map(float, ll.split())
  176. xmax, ymax, zmax = map(float, ur.split())
  177. return (xmin, ymin, zmin, xmax, ymax, zmax)
  178. def convert_geom(self, hex, geo_field):
  179. """
  180. Converts the geometry returned from PostGIS aggretates.
  181. """
  182. if hex:
  183. return Geometry(hex, srid=geo_field.srid)
  184. else:
  185. return None
  186. def geo_db_type(self, f):
  187. """
  188. Return the database field type for the given spatial field.
  189. """
  190. if f.geom_type == 'RASTER':
  191. return 'raster'
  192. elif f.geography:
  193. if f.srid != 4326:
  194. raise NotImplementedError('PostGIS only supports geography columns with an SRID of 4326.')
  195. return 'geography(%s,%d)' % (f.geom_type, f.srid)
  196. else:
  197. # Type-based geometries.
  198. # TODO: Support 'M' extension.
  199. if f.dim == 3:
  200. geom_type = f.geom_type + 'Z'
  201. else:
  202. geom_type = f.geom_type
  203. return 'geometry(%s,%d)' % (geom_type, f.srid)
  204. def get_distance(self, f, dist_val, lookup_type, handle_spheroid=True):
  205. """
  206. Retrieve the distance parameters for the given geometry field,
  207. distance lookup value, and the distance lookup type.
  208. This is the most complex implementation of the spatial backends due to
  209. what is supported on geodetic geometry columns vs. what's available on
  210. projected geometry columns. In addition, it has to take into account
  211. the geography column type.
  212. """
  213. # Getting the distance parameter
  214. value = dist_val[0]
  215. # Shorthand boolean flags.
  216. geodetic = f.geodetic(self.connection)
  217. geography = f.geography
  218. if isinstance(value, Distance):
  219. if geography:
  220. dist_param = value.m
  221. elif geodetic:
  222. if lookup_type == 'dwithin':
  223. raise ValueError('Only numeric values of degree units are '
  224. 'allowed on geographic DWithin queries.')
  225. dist_param = value.m
  226. else:
  227. dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
  228. else:
  229. # Assuming the distance is in the units of the field.
  230. dist_param = value
  231. params = [dist_param]
  232. # handle_spheroid *might* be dropped in Django 2.0 as PostGISDistanceOperator
  233. # also handles it (#25524).
  234. if handle_spheroid and len(dist_val) > 1:
  235. option = dist_val[1]
  236. if (not geography and geodetic and lookup_type != 'dwithin'
  237. and option == 'spheroid'):
  238. # using distance_spheroid requires the spheroid of the field as
  239. # a parameter.
  240. params.insert(0, f._spheroid)
  241. return params
  242. def get_geom_placeholder(self, f, value, compiler):
  243. """
  244. Provides a proper substitution value for Geometries that are not in the
  245. SRID of the field. Specifically, this routine will substitute in the
  246. ST_Transform() function call.
  247. """
  248. # Get the srid for this object
  249. if value is None:
  250. value_srid = None
  251. elif f.geom_type == 'RASTER':
  252. value_srid = get_pgraster_srid(value)
  253. else:
  254. value_srid = value.srid
  255. # Adding Transform() to the SQL placeholder if the value srid
  256. # is not equal to the field srid.
  257. if value_srid is None or value_srid == f.srid:
  258. placeholder = '%s'
  259. elif f.geom_type == 'RASTER':
  260. placeholder = '%s((%%s)::raster, %s)' % (self.transform, f.srid)
  261. else:
  262. placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
  263. if hasattr(value, 'as_sql'):
  264. # If this is an F expression, then we don't really want
  265. # a placeholder and instead substitute in the column
  266. # of the expression.
  267. sql, _ = compiler.compile(value)
  268. placeholder = placeholder % sql
  269. return placeholder
  270. def _get_postgis_func(self, func):
  271. """
  272. Helper routine for calling PostGIS functions and returning their result.
  273. """
  274. # Close out the connection. See #9437.
  275. with self.connection.temporary_connection() as cursor:
  276. cursor.execute('SELECT %s()' % func)
  277. return cursor.fetchone()[0]
  278. def postgis_geos_version(self):
  279. "Returns the version of the GEOS library used with PostGIS."
  280. return self._get_postgis_func('postgis_geos_version')
  281. def postgis_lib_version(self):
  282. "Returns the version number of the PostGIS library used with PostgreSQL."
  283. return self._get_postgis_func('postgis_lib_version')
  284. def postgis_proj_version(self):
  285. "Returns the version of the PROJ.4 library used with PostGIS."
  286. return self._get_postgis_func('postgis_proj_version')
  287. def postgis_version(self):
  288. "Returns PostGIS version number and compile-time options."
  289. return self._get_postgis_func('postgis_version')
  290. def postgis_full_version(self):
  291. "Returns PostGIS version number and compile-time options."
  292. return self._get_postgis_func('postgis_full_version')
  293. def postgis_version_tuple(self):
  294. """
  295. Returns the PostGIS version as a tuple (version string, major,
  296. minor, subminor).
  297. """
  298. # Getting the PostGIS version
  299. version = self.postgis_lib_version()
  300. m = self.version_regex.match(version)
  301. if m:
  302. major = int(m.group('major'))
  303. minor1 = int(m.group('minor1'))
  304. minor2 = int(m.group('minor2'))
  305. else:
  306. raise Exception('Could not parse PostGIS version string: %s' % version)
  307. return (version, major, minor1, minor2)
  308. def proj_version_tuple(self):
  309. """
  310. Return the version of PROJ.4 used by PostGIS as a tuple of the
  311. major, minor, and subminor release numbers.
  312. """
  313. proj_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)')
  314. proj_ver_str = self.postgis_proj_version()
  315. m = proj_regex.search(proj_ver_str)
  316. if m:
  317. return tuple(map(int, [m.group(1), m.group(2), m.group(3)]))
  318. else:
  319. raise Exception('Could not determine PROJ.4 version from PostGIS.')
  320. def spatial_aggregate_name(self, agg_name):
  321. if agg_name == 'Extent3D':
  322. return self.extent3d
  323. else:
  324. return self.geom_func_prefix + agg_name
  325. # Routines for getting the OGC-compliant models.
  326. def geometry_columns(self):
  327. return PostGISGeometryColumns
  328. def spatial_ref_sys(self):
  329. return PostGISSpatialRefSys
  330. # Methods to convert between PostGIS rasters and dicts that are
  331. # readable by GDALRaster.
  332. def parse_raster(self, value):
  333. return from_pgraster(value)
  334. def deconstruct_raster(self, value):
  335. return to_pgraster(value)