测试gitnore

This commit is contained in:
ladeng07
2022-05-06 15:45:57 +08:00
parent 12f390949b
commit 51552904f9
2347 changed files with 120102 additions and 53549 deletions
@@ -2,16 +2,14 @@ class WKTAdapter:
"""
An adaptor for Geometries sent to the MySQL and Oracle database backends.
"""
def __init__(self, geom):
self.wkt = geom.wkt
self.srid = geom.srid
def __eq__(self, other):
return (
isinstance(other, WKTAdapter)
and self.wkt == other.wkt
and self.srid == other.srid
isinstance(other, WKTAdapter) and
self.wkt == other.wkt and self.srid == other.srid
)
def __hash__(self):
@@ -60,15 +60,15 @@ class BaseSpatialFeatures:
@property
def supports_bbcontains_lookup(self):
return "bbcontains" in self.connection.ops.gis_operators
return 'bbcontains' in self.connection.ops.gis_operators
@property
def supports_contained_lookup(self):
return "contained" in self.connection.ops.gis_operators
return 'contained' in self.connection.ops.gis_operators
@property
def supports_crosses_lookup(self):
return "crosses" in self.connection.ops.gis_operators
return 'crosses' in self.connection.ops.gis_operators
@property
def supports_distances_lookups(self):
@@ -76,11 +76,11 @@ class BaseSpatialFeatures:
@property
def supports_dwithin_lookup(self):
return "dwithin" in self.connection.ops.gis_operators
return 'dwithin' in self.connection.ops.gis_operators
@property
def supports_relate_lookup(self):
return "relate" in self.connection.ops.gis_operators
return 'relate' in self.connection.ops.gis_operators
@property
def supports_isvalid_lookup(self):
@@ -104,7 +104,7 @@ class BaseSpatialFeatures:
return models.Union not in self.connection.ops.disallowed_aggregates
def __getattr__(self, name):
m = re.match(r"has_(\w*)_function$", name)
m = re.match(r'has_(\w*)_function$', name)
if m:
func_name = m[1]
return func_name not in self.connection.ops.unsupported_functions
@@ -6,14 +6,13 @@ class SpatialRefSysMixin:
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundant code.
"""
@property
def srs(self):
"""
Return a GDAL SpatialReference object.
"""
# TODO: Is caching really necessary here? Is complexity worth it?
if hasattr(self, "_srs"):
if hasattr(self, '_srs'):
# Returning a clone of the cached SpatialReference object.
return self._srs.clone()
else:
@@ -32,10 +31,7 @@ class SpatialRefSysMixin:
except Exception as e:
msg = e
raise Exception(
"Could not get OSR SpatialReference from WKT: %s\nError:\n%s"
% (self.wkt, msg)
)
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
@property
def ellipsoid(self):
@@ -53,12 +49,12 @@ class SpatialRefSysMixin:
@property
def spheroid(self):
"Return the spheroid name for this spatial reference."
return self.srs["spheroid"]
return self.srs['spheroid']
@property
def datum(self):
"Return the datum for this spatial reference."
return self.srs["datum"]
return self.srs['datum']
@property
def projected(self):
@@ -121,7 +117,7 @@ class SpatialRefSysMixin:
"""
srs = gdal.SpatialReference(wkt)
sphere_params = srs.ellipsoid
sphere_name = srs["spheroid"]
sphere_name = srs['spheroid']
if not string:
return sphere_name, sphere_params
@@ -1,7 +1,8 @@
from django.contrib.gis.db.models import GeometryField
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import Area as AreaMeasure
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.contrib.gis.measure import (
Area as AreaMeasure, Distance as DistanceMeasure,
)
from django.db import NotSupportedError
from django.utils.functional import cached_property
@@ -17,7 +18,7 @@ class BaseSpatialOperations:
spatial_version = None
# How the geometry column should be selected.
select = "%s"
select = '%s'
@cached_property
def select_extent(self):
@@ -26,7 +27,7 @@ class BaseSpatialOperations:
# Aggregates
disallowed_aggregates = ()
geom_func_prefix = ""
geom_func_prefix = ''
# Mapping between Django function names and backend names, when names do not
# match; used in spatial_function_name().
@@ -34,36 +35,12 @@ class BaseSpatialOperations:
# Set of known unsupported functions of the backend
unsupported_functions = {
"Area",
"AsGeoJSON",
"AsGML",
"AsKML",
"AsSVG",
"Azimuth",
"BoundingCircle",
"Centroid",
"Difference",
"Distance",
"Envelope",
"GeoHash",
"GeometryDistance",
"Intersection",
"IsValid",
"Length",
"LineLocatePoint",
"MakeValid",
"MemSize",
"NumGeometries",
"NumPoints",
"Perimeter",
"PointOnSurface",
"Reverse",
"Scale",
"SnapToGrid",
"SymDifference",
"Transform",
"Translate",
"Union",
'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'Azimuth',
'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope',
'GeoHash', 'GeometryDistance', 'Intersection', 'IsValid', 'Length',
'LineLocatePoint', 'MakeValid', 'MemSize', 'NumGeometries',
'NumPoints', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale',
'SnapToGrid', 'SymDifference', 'Transform', 'Translate', 'Union',
}
# Constructors
@@ -72,14 +49,10 @@ class BaseSpatialOperations:
# Default conversion functions for aggregates; will be overridden if implemented
# for the spatial backend.
def convert_extent(self, box, srid):
raise NotImplementedError(
"Aggregate extent not implemented for this spatial backend."
)
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
def convert_extent3d(self, box, srid):
raise NotImplementedError(
"Aggregate 3D extent not implemented for this spatial backend."
)
raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.')
# For quoting column values, rather than columns.
def geo_quote_name(self, name):
@@ -91,18 +64,14 @@ class BaseSpatialOperations:
Return the database column type for the geometry field on
the spatial backend.
"""
raise NotImplementedError(
"subclasses of BaseSpatialOperations must provide a geo_db_type() method"
)
raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_type() method')
def get_distance(self, f, value, lookup_type):
"""
Return the distance parameters for the given geometry field,
lookup value, and lookup type.
"""
raise NotImplementedError(
"Distance operations not available on this spatial backend."
)
raise NotImplementedError('Distance operations not available on this spatial backend.')
def get_geom_placeholder(self, f, value, compiler):
"""
@@ -111,62 +80,48 @@ class BaseSpatialOperations:
stored procedure call to the transformation function of the spatial
backend.
"""
def transform_value(value, field):
return value is not None and value.srid != field.srid
if hasattr(value, "as_sql"):
if hasattr(value, 'as_sql'):
return (
"%s(%%s, %s)" % (self.spatial_function_name("Transform"), f.srid)
'%s(%%s, %s)' % (self.spatial_function_name('Transform'), f.srid)
if transform_value(value.output_field, f)
else "%s"
else '%s'
)
if transform_value(value, f):
# Add Transform() to the SQL placeholder.
return "%s(%s(%%s,%s), %s)" % (
self.spatial_function_name("Transform"),
self.from_text,
value.srid,
f.srid,
return '%s(%s(%%s,%s), %s)' % (
self.spatial_function_name('Transform'),
self.from_text, value.srid, f.srid,
)
elif self.connection.features.has_spatialrefsys_table:
return "%s(%%s,%s)" % (self.from_text, f.srid)
return '%s(%%s,%s)' % (self.from_text, f.srid)
else:
# For backwards compatibility on MySQL (#27464).
return "%s(%%s)" % self.from_text
return '%s(%%s)' % self.from_text
def check_expression_support(self, expression):
if isinstance(expression, self.disallowed_aggregates):
raise NotSupportedError(
"%s spatial aggregation is not supported by this database backend."
% expression.name
"%s spatial aggregation is not supported by this database backend." % expression.name
)
super().check_expression_support(expression)
def spatial_aggregate_name(self, agg_name):
raise NotImplementedError(
"Aggregate support not implemented for this spatial backend."
)
raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
def spatial_function_name(self, func_name):
if func_name in self.unsupported_functions:
raise NotSupportedError(
"This backend doesn't support the %s function." % func_name
)
raise NotSupportedError("This backend doesn't support the %s function." % func_name)
return self.function_names.get(func_name, self.geom_func_prefix + func_name)
# Routines for getting the OGC-compliant models.
def geometry_columns(self):
raise NotImplementedError(
"Subclasses of BaseSpatialOperations must provide a geometry_columns() "
"method."
)
raise NotImplementedError('Subclasses of BaseSpatialOperations must provide a geometry_columns() method.')
def spatial_ref_sys(self):
raise NotImplementedError(
"subclasses of BaseSpatialOperations must a provide spatial_ref_sys() "
"method"
)
raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method')
distance_expr_for_lookup = staticmethod(Distance)
@@ -178,17 +133,15 @@ class BaseSpatialOperations:
def get_geometry_converter(self, expression):
raise NotImplementedError(
"Subclasses of BaseSpatialOperations must provide a "
"get_geometry_converter() method."
'Subclasses of BaseSpatialOperations must provide a '
'get_geometry_converter() method.'
)
def get_area_att_for_field(self, field):
if field.geodetic(self.connection):
if self.connection.features.supports_area_geodetic:
return "sq_m"
raise NotImplementedError(
"Area on geodetic coordinate systems not supported."
)
return 'sq_m'
raise NotImplementedError('Area on geodetic coordinate systems not supported.')
else:
units_name = field.units_name(self.connection)
if units_name:
@@ -198,7 +151,7 @@ class BaseSpatialOperations:
dist_att = None
if field.geodetic(self.connection):
if self.connection.features.supports_distance_geodetic:
dist_att = "m"
dist_att = 'm'
else:
units = field.units_name(self.connection)
if units:
@@ -1,4 +1,6 @@
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
from django.db.backends.mysql.base import (
DatabaseWrapper as MySQLDatabaseWrapper,
)
from .features import DatabaseFeatures
from .introspection import MySQLIntrospection
@@ -1,5 +1,7 @@
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures
from django.db.backends.mysql.features import (
DatabaseFeatures as MySQLDatabaseFeatures,
)
from django.utils.functional import cached_property
@@ -12,13 +14,13 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
supports_transform = False
supports_null_geometries = False
supports_num_points_poly = False
unsupported_geojson_options = {"crs"}
unsupported_geojson_options = {'crs'}
@cached_property
def empty_intersection_returns_none(self):
return (
not self.connection.mysql_is_mariadb
and self.connection.mysql_version < (5, 7, 5)
not self.connection.mysql_is_mariadb and
self.connection.mysql_version < (5, 7, 5)
)
@cached_property
@@ -29,16 +31,13 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
@cached_property
def django_test_skips(self):
skips = super().django_test_skips
if not self.connection.mysql_is_mariadb and self.connection.mysql_version < (
8,
0,
0,
if (
not self.connection.mysql_is_mariadb and
self.connection.mysql_version < (8, 0, 0)
):
skips.update(
{
"MySQL < 8 gives different results.": {
"gis_tests.geoapp.tests.GeoLookupTest.test_disjoint_lookup",
},
}
)
skips.update({
'MySQL < 8 gives different results.': {
'gis_tests.geoapp.tests.GeoLookupTest.test_disjoint_lookup',
},
})
return skips
@@ -8,13 +8,14 @@ class MySQLIntrospection(DatabaseIntrospection):
# Updating the data_types_reverse dictionary with the appropriate
# type for Geometry fields.
data_types_reverse = DatabaseIntrospection.data_types_reverse.copy()
data_types_reverse[FIELD_TYPE.GEOMETRY] = "GeometryField"
data_types_reverse[FIELD_TYPE.GEOMETRY] = 'GeometryField'
def get_geometry_type(self, table_name, description):
with self.connection.cursor() as cursor:
# In order to get the specific geometry type of the field,
# we introspect on the table definition using `DESCRIBE`.
cursor.execute("DESCRIBE %s" % self.connection.ops.quote_name(table_name))
cursor.execute('DESCRIBE %s' %
self.connection.ops.quote_name(table_name))
# Increment over description info until we get to the geometry
# column.
for column, typ, null, key, default, extra in cursor.fetchall():
@@ -30,8 +31,8 @@ class MySQLIntrospection(DatabaseIntrospection):
def supports_spatial_index(self, cursor, table_name):
# Supported with MyISAM/Aria, or InnoDB on MySQL 5.7.5+/MariaDB 10.2.2+
storage_engine = self.get_storage_engine(cursor, table_name)
if storage_engine == "InnoDB":
if storage_engine == 'InnoDB':
return self.connection.mysql_version >= (
(10, 2, 2) if self.connection.mysql_is_mariadb else (5, 7, 5)
)
return storage_engine in ("MyISAM", "Aria")
return storage_engine in ('MyISAM', 'Aria')
@@ -1,6 +1,8 @@
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.db.backends.base.operations import (
BaseSpatialOperations,
)
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.geos.geometry import GEOSGeometryBase
from django.contrib.gis.geos.prototypes.io import wkb_r
@@ -10,8 +12,8 @@ from django.utils.functional import cached_property
class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
name = "mysql"
geom_func_prefix = "ST_"
name = 'mysql'
geom_func_prefix = 'ST_'
Adapter = WKTAdapter
@@ -25,71 +27,53 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
@cached_property
def select(self):
return self.geom_func_prefix + "AsBinary(%s)"
return self.geom_func_prefix + 'AsBinary(%s)'
@cached_property
def from_text(self):
return self.geom_func_prefix + "GeomFromText"
return self.geom_func_prefix + 'GeomFromText'
@cached_property
def gis_operators(self):
operators = {
"bbcontains": SpatialOperator(
func="MBRContains"
), # For consistency w/PostGIS API
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
"contained": SpatialOperator(func="MBRWithin"), # ...
"contains": SpatialOperator(func="ST_Contains"),
"crosses": SpatialOperator(func="ST_Crosses"),
"disjoint": SpatialOperator(func="ST_Disjoint"),
"equals": SpatialOperator(func="ST_Equals"),
"exact": SpatialOperator(func="ST_Equals"),
"intersects": SpatialOperator(func="ST_Intersects"),
"overlaps": SpatialOperator(func="ST_Overlaps"),
"same_as": SpatialOperator(func="ST_Equals"),
"touches": SpatialOperator(func="ST_Touches"),
"within": SpatialOperator(func="ST_Within"),
'bbcontains': SpatialOperator(func='MBRContains'), # For consistency w/PostGIS API
'bboverlaps': SpatialOperator(func='MBROverlaps'), # ...
'contained': SpatialOperator(func='MBRWithin'), # ...
'contains': SpatialOperator(func='ST_Contains'),
'crosses': SpatialOperator(func='ST_Crosses'),
'disjoint': SpatialOperator(func='ST_Disjoint'),
'equals': SpatialOperator(func='ST_Equals'),
'exact': SpatialOperator(func='ST_Equals'),
'intersects': SpatialOperator(func='ST_Intersects'),
'overlaps': SpatialOperator(func='ST_Overlaps'),
'same_as': SpatialOperator(func='ST_Equals'),
'touches': SpatialOperator(func='ST_Touches'),
'within': SpatialOperator(func='ST_Within'),
}
if self.connection.mysql_is_mariadb:
operators["relate"] = SpatialOperator(func="ST_Relate")
operators['relate'] = SpatialOperator(func='ST_Relate')
return operators
disallowed_aggregates = (
models.Collect,
models.Extent,
models.Extent3D,
models.MakeLine,
models.Collect, models.Extent, models.Extent3D, models.MakeLine,
models.Union,
)
@cached_property
def unsupported_functions(self):
unsupported = {
"AsGML",
"AsKML",
"AsSVG",
"Azimuth",
"BoundingCircle",
"ForcePolygonCW",
"GeometryDistance",
"LineLocatePoint",
"MakeValid",
"MemSize",
"Perimeter",
"PointOnSurface",
"Reverse",
"Scale",
"SnapToGrid",
"Transform",
"Translate",
'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle',
'ForcePolygonCW', 'GeometryDistance', 'LineLocatePoint',
'MakeValid', 'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse',
'Scale', 'SnapToGrid', 'Transform', 'Translate',
}
if self.connection.mysql_is_mariadb:
unsupported.remove("PointOnSurface")
unsupported.update({"GeoHash", "IsValid"})
unsupported.remove('PointOnSurface')
unsupported.update({'GeoHash', 'IsValid'})
if self.connection.mysql_version < (10, 2, 4):
unsupported.add("AsGeoJSON")
unsupported.add('AsGeoJSON')
elif self.connection.mysql_version < (5, 7, 5):
unsupported.update({"AsGeoJSON", "GeoHash", "IsValid"})
unsupported.update({'AsGeoJSON', 'GeoHash', 'IsValid'})
return unsupported
def geo_db_type(self, f):
@@ -100,12 +84,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
if isinstance(value, Distance):
if f.geodetic(self.connection):
raise ValueError(
"Only numeric values of degree units are allowed on "
"geodetic distance queries."
'Only numeric values of degree units are allowed on '
'geodetic distance queries.'
)
dist_param = getattr(
value, Distance.unit_attname(f.units_name(self.connection))
)
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
else:
dist_param = value
return [dist_param]
@@ -123,5 +105,4 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
if srid:
geom.srid = srid
return geom
return converter
@@ -4,12 +4,12 @@ from django.contrib.gis.db.models import GeometryField
from django.db import OperationalError
from django.db.backends.mysql.schema import DatabaseSchemaEditor
logger = logging.getLogger("django.contrib.gis")
logger = logging.getLogger('django.contrib.gis')
class MySQLGISSchemaEditor(DatabaseSchemaEditor):
sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)"
sql_drop_spatial_index = "DROP INDEX %(index)s ON %(table)s"
sql_add_spatial_index = 'CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)'
sql_drop_spatial_index = 'DROP INDEX %(index)s ON %(table)s'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -18,10 +18,7 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor):
def skip_default(self, field):
# Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13 and
# MariaDB < 10.2.1 don't support defaults.
if (
isinstance(field, GeometryField)
and not self._supports_limited_data_type_defaults
):
if isinstance(field, GeometryField) and not self._supports_limited_data_type_defaults:
return True
return super().skip_default(field)
@@ -32,11 +29,10 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor):
qn = self.connection.ops.quote_name
db_table = model._meta.db_table
self.geometry_sql.append(
self.sql_add_spatial_index
% {
"index": qn(self._create_spatial_index_name(model, field)),
"table": qn(db_table),
"column": qn(field.column),
self.sql_add_spatial_index % {
'index': qn(self._create_spatial_index_name(model, field)),
'table': qn(db_table),
'column': qn(field.column),
}
)
return column_sql
@@ -53,22 +49,21 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor):
if isinstance(field, GeometryField) and field.spatial_index:
qn = self.connection.ops.quote_name
sql = self.sql_drop_spatial_index % {
"index": qn(self._create_spatial_index_name(model, field)),
"table": qn(model._meta.db_table),
'index': qn(self._create_spatial_index_name(model, field)),
'table': qn(model._meta.db_table),
}
try:
self.execute(sql)
except OperationalError:
logger.error(
"Couldn't remove spatial index: %s (may be expected "
"if your storage engine doesn't support them).",
sql,
"if your storage engine doesn't support them).", sql
)
super().remove_field(model, field)
def _create_spatial_index_name(self, model, field):
return "%s_%s_id" % (model._meta.db_table, field.column)
return '%s_%s_id' % (model._meta.db_table, field.column)
def create_spatial_indexes(self):
for sql in self.geometry_sql:
@@ -77,7 +72,6 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor):
except OperationalError:
logger.error(
"Cannot create SPATIAL INDEX %s. Only MyISAM and (as of "
"MySQL 5.7.5) InnoDB support them.",
sql,
"MySQL 5.7.5) InnoDB support them.", sql
)
self.geometry_sql = []
@@ -19,9 +19,7 @@ class OracleSpatialAdapter(WKTAdapter):
if self._polygon_must_be_fixed(geom):
geom = self._fix_polygon(geom)
elif isinstance(geom, GeometryCollection):
if any(
isinstance(g, Polygon) and self._polygon_must_be_fixed(g) for g in geom
):
if any(isinstance(g, Polygon) and self._polygon_must_be_fixed(g) for g in geom):
geom = self._fix_geometry_collection(geom)
self.wkt = geom.wkt
@@ -29,9 +27,12 @@ class OracleSpatialAdapter(WKTAdapter):
@staticmethod
def _polygon_must_be_fixed(poly):
return not poly.empty and (
not poly.exterior_ring.is_counterclockwise
or any(x.is_counterclockwise for x in poly)
return (
not poly.empty and
(
not poly.exterior_ring.is_counterclockwise or
any(x.is_counterclockwise for x in poly)
)
)
@classmethod
@@ -1,4 +1,6 @@
from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper
from django.db.backends.oracle.base import (
DatabaseWrapper as OracleDatabaseWrapper,
)
from .features import DatabaseFeatures
from .introspection import OracleIntrospection
@@ -11,4 +11,4 @@ class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures):
supports_perimeter_geodetic = True
supports_dwithin_distance_expr = False
supports_tolerance_parameter = True
unsupported_geojson_options = {"bbox", "crs", "precision"}
unsupported_geojson_options = {'bbox', 'crs', 'precision'}
@@ -12,7 +12,7 @@ class OracleIntrospection(DatabaseIntrospection):
def data_types_reverse(self):
return {
**super().data_types_reverse,
cx_Oracle.OBJECT: "GeometryField",
cx_Oracle.OBJECT: 'GeometryField',
}
def get_geometry_type(self, table_name, description):
@@ -22,26 +22,26 @@ class OracleIntrospection(DatabaseIntrospection):
cursor.execute(
'SELECT "DIMINFO", "SRID" FROM "USER_SDO_GEOM_METADATA" '
'WHERE "TABLE_NAME"=%s AND "COLUMN_NAME"=%s',
(table_name.upper(), description.name.upper()),
(table_name.upper(), description.name.upper())
)
row = cursor.fetchone()
except Exception as exc:
raise Exception(
"Could not find entry in USER_SDO_GEOM_METADATA "
'Could not find entry in USER_SDO_GEOM_METADATA '
'corresponding to "%s"."%s"' % (table_name, description.name)
) from exc
# TODO: Research way to find a more specific geometry field type for
# the column's contents.
field_type = "GeometryField"
field_type = 'GeometryField'
# Getting the field parameters.
field_params = {}
dim, srid = row
if srid != 4326:
field_params["srid"] = srid
field_params['srid'] = srid
# Size of object array (SDO_DIM_ARRAY) is number of dimensions.
dim = dim.size()
if dim != 2:
field_params["dim"] = dim
field_params['dim'] = dim
return field_type, field_params
@@ -19,12 +19,12 @@ class OracleGeometryColumns(models.Model):
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
class Meta:
app_label = "gis"
db_table = "USER_SDO_GEOM_METADATA"
app_label = 'gis'
db_table = 'USER_SDO_GEOM_METADATA'
managed = False
def __str__(self):
return "%s - %s (SRID: %s)" % (self.table_name, self.column_name, self.srid)
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
@classmethod
def table_name_col(cls):
@@ -32,7 +32,7 @@ class OracleGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature table
name.
"""
return "table_name"
return 'table_name'
@classmethod
def geom_col_name(cls):
@@ -40,7 +40,7 @@ class OracleGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature
geometry column.
"""
return "column_name"
return 'column_name'
class OracleSpatialRefSys(models.Model, SpatialRefSysMixin):
@@ -55,8 +55,8 @@ class OracleSpatialRefSys(models.Model, SpatialRefSysMixin):
cs_bounds = models.PolygonField(null=True)
class Meta:
app_label = "gis"
db_table = "CS_SRS"
app_label = 'gis'
db_table = 'CS_SRS'
managed = False
@property
@@ -10,7 +10,9 @@
import re
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.db.backends.base.operations import (
BaseSpatialOperations,
)
from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
@@ -18,7 +20,7 @@ from django.contrib.gis.geos.prototypes.io import wkb_r
from django.contrib.gis.measure import Distance
from django.db.backends.oracle.operations import DatabaseOperations
DEFAULT_TOLERANCE = "0.05"
DEFAULT_TOLERANCE = '0.05'
class SDOOperator(SpatialOperator):
@@ -30,63 +32,57 @@ class SDODWithin(SpatialOperator):
class SDODisjoint(SpatialOperator):
sql_template = (
"SDO_GEOM.RELATE(%%(lhs)s, 'DISJOINT', %%(rhs)s, %s) = 'DISJOINT'"
% DEFAULT_TOLERANCE
)
sql_template = "SDO_GEOM.RELATE(%%(lhs)s, 'DISJOINT', %%(rhs)s, %s) = 'DISJOINT'" % DEFAULT_TOLERANCE
class SDORelate(SpatialOperator):
sql_template = "SDO_RELATE(%(lhs)s, %(rhs)s, 'mask=%(mask)s') = 'TRUE'"
def check_relate_argument(self, arg):
masks = (
"TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|"
"CONTAINS|COVERS|ANYINTERACT|ON"
)
mask_regex = re.compile(r"^(%s)(\+(%s))*$" % (masks, masks), re.I)
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
if not isinstance(arg, str) or not mask_regex.match(arg):
raise ValueError('Invalid SDO_RELATE mask: "%s"' % arg)
def as_sql(self, connection, lookup, template_params, sql_params):
template_params["mask"] = sql_params[-1]
template_params['mask'] = sql_params[-1]
return super().as_sql(connection, lookup, template_params, sql_params[:-1])
class OracleOperations(BaseSpatialOperations, DatabaseOperations):
name = "oracle"
name = 'oracle'
oracle = True
disallowed_aggregates = (models.Collect, models.Extent3D, models.MakeLine)
Adapter = OracleSpatialAdapter
extent = "SDO_AGGR_MBR"
unionagg = "SDO_AGGR_UNION"
extent = 'SDO_AGGR_MBR'
unionagg = 'SDO_AGGR_UNION'
from_text = "SDO_GEOMETRY"
from_text = 'SDO_GEOMETRY'
function_names = {
"Area": "SDO_GEOM.SDO_AREA",
"AsGeoJSON": "SDO_UTIL.TO_GEOJSON",
"AsWKB": "SDO_UTIL.TO_WKBGEOMETRY",
"AsWKT": "SDO_UTIL.TO_WKTGEOMETRY",
"BoundingCircle": "SDO_GEOM.SDO_MBC",
"Centroid": "SDO_GEOM.SDO_CENTROID",
"Difference": "SDO_GEOM.SDO_DIFFERENCE",
"Distance": "SDO_GEOM.SDO_DISTANCE",
"Envelope": "SDO_GEOM_MBR",
"Intersection": "SDO_GEOM.SDO_INTERSECTION",
"IsValid": "SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT",
"Length": "SDO_GEOM.SDO_LENGTH",
"NumGeometries": "SDO_UTIL.GETNUMELEM",
"NumPoints": "SDO_UTIL.GETNUMVERTICES",
"Perimeter": "SDO_GEOM.SDO_LENGTH",
"PointOnSurface": "SDO_GEOM.SDO_POINTONSURFACE",
"Reverse": "SDO_UTIL.REVERSE_LINESTRING",
"SymDifference": "SDO_GEOM.SDO_XOR",
"Transform": "SDO_CS.TRANSFORM",
"Union": "SDO_GEOM.SDO_UNION",
'Area': 'SDO_GEOM.SDO_AREA',
'AsGeoJSON': 'SDO_UTIL.TO_GEOJSON',
'AsWKB': 'SDO_UTIL.TO_WKBGEOMETRY',
'AsWKT': 'SDO_UTIL.TO_WKTGEOMETRY',
'BoundingCircle': 'SDO_GEOM.SDO_MBC',
'Centroid': 'SDO_GEOM.SDO_CENTROID',
'Difference': 'SDO_GEOM.SDO_DIFFERENCE',
'Distance': 'SDO_GEOM.SDO_DISTANCE',
'Envelope': 'SDO_GEOM_MBR',
'Intersection': 'SDO_GEOM.SDO_INTERSECTION',
'IsValid': 'SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT',
'Length': 'SDO_GEOM.SDO_LENGTH',
'NumGeometries': 'SDO_UTIL.GETNUMELEM',
'NumPoints': 'SDO_UTIL.GETNUMVERTICES',
'Perimeter': 'SDO_GEOM.SDO_LENGTH',
'PointOnSurface': 'SDO_GEOM.SDO_POINTONSURFACE',
'Reverse': 'SDO_UTIL.REVERSE_LINESTRING',
'SymDifference': 'SDO_GEOM.SDO_XOR',
'Transform': 'SDO_CS.TRANSFORM',
'Union': 'SDO_GEOM.SDO_UNION',
}
# We want to get SDO Geometries as WKT because it is much easier to
@@ -94,40 +90,28 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
# However, this adversely affects performance (i.e., Java is called
# to convert to WKT on every query). If someone wishes to write a
# SDO_GEOMETRY(...) parser in Python, let me know =)
select = "SDO_UTIL.TO_WKBGEOMETRY(%s)"
select = 'SDO_UTIL.TO_WKBGEOMETRY(%s)'
gis_operators = {
"contains": SDOOperator(func="SDO_CONTAINS"),
"coveredby": SDOOperator(func="SDO_COVEREDBY"),
"covers": SDOOperator(func="SDO_COVERS"),
"disjoint": SDODisjoint(),
"intersects": SDOOperator(
func="SDO_OVERLAPBDYINTERSECT"
), # TODO: Is this really the same as ST_Intersects()?
"equals": SDOOperator(func="SDO_EQUAL"),
"exact": SDOOperator(func="SDO_EQUAL"),
"overlaps": SDOOperator(func="SDO_OVERLAPS"),
"same_as": SDOOperator(func="SDO_EQUAL"),
# Oracle uses a different syntax, e.g., 'mask=inside+touch'
"relate": SDORelate(),
"touches": SDOOperator(func="SDO_TOUCH"),
"within": SDOOperator(func="SDO_INSIDE"),
"dwithin": SDODWithin(),
'contains': SDOOperator(func='SDO_CONTAINS'),
'coveredby': SDOOperator(func='SDO_COVEREDBY'),
'covers': SDOOperator(func='SDO_COVERS'),
'disjoint': SDODisjoint(),
'intersects': SDOOperator(func='SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals': SDOOperator(func='SDO_EQUAL'),
'exact': SDOOperator(func='SDO_EQUAL'),
'overlaps': SDOOperator(func='SDO_OVERLAPS'),
'same_as': SDOOperator(func='SDO_EQUAL'),
'relate': SDORelate(), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
'touches': SDOOperator(func='SDO_TOUCH'),
'within': SDOOperator(func='SDO_INSIDE'),
'dwithin': SDODWithin(),
}
unsupported_functions = {
"AsKML",
"AsSVG",
"Azimuth",
"ForcePolygonCW",
"GeoHash",
"GeometryDistance",
"LineLocatePoint",
"MakeValid",
"MemSize",
"Scale",
"SnapToGrid",
"Translate",
'AsKML', 'AsSVG', 'Azimuth', 'ForcePolygonCW', 'GeoHash',
'GeometryDistance', 'LineLocatePoint', 'MakeValid', 'MemSize',
'Scale', 'SnapToGrid', 'Translate',
}
def geo_quote_name(self, name):
@@ -140,17 +124,15 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
# table.
ext_geom = GEOSGeometry(memoryview(clob.read()))
gtype = str(ext_geom.geom_type)
if gtype == "Polygon":
if gtype == 'Polygon':
# Construct the 4-tuple from the coordinates in the polygon.
shell = ext_geom.shell
ll, ur = shell[0][:2], shell[2][:2]
elif gtype == "Point":
elif gtype == 'Point':
ll = ext_geom.coords[:2]
ur = ll
else:
raise Exception(
"Unexpected geometry type returned for extent: %s" % gtype
)
raise Exception('Unexpected geometry type returned for extent: %s' % gtype)
xmin, ymin = ll
xmax, ymax = ur
return (xmin, ymin, xmax, ymax)
@@ -163,7 +145,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
backends, no stored procedure is necessary and it's the same for all
geometry types.
"""
return "MDSYS.SDO_GEOMETRY"
return 'MDSYS.SDO_GEOMETRY'
def get_distance(self, f, value, lookup_type):
"""
@@ -179,47 +161,47 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
if f.geodetic(self.connection):
dist_param = value.m
else:
dist_param = getattr(
value, Distance.unit_attname(f.units_name(self.connection))
)
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
else:
dist_param = value
# dwithin lookups on Oracle require a special string parameter
# that starts with "distance=".
if lookup_type == "dwithin":
dist_param = "distance=%s" % dist_param
if lookup_type == 'dwithin':
dist_param = 'distance=%s' % dist_param
return [dist_param]
def get_geom_placeholder(self, f, value, compiler):
if value is None:
return "NULL"
return 'NULL'
return super().get_geom_placeholder(f, value, compiler)
def spatial_aggregate_name(self, agg_name):
"""
Return the spatial aggregate SQL name.
"""
agg_name = "unionagg" if agg_name.lower() == "union" else agg_name.lower()
agg_name = 'unionagg' if agg_name.lower() == 'union' else agg_name.lower()
return getattr(self, agg_name)
# Routines for getting the OGC-compliant models.
def geometry_columns(self):
from django.contrib.gis.db.backends.oracle.models import OracleGeometryColumns
from django.contrib.gis.db.backends.oracle.models import (
OracleGeometryColumns,
)
return OracleGeometryColumns
def spatial_ref_sys(self):
from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys
from django.contrib.gis.db.backends.oracle.models import (
OracleSpatialRefSys,
)
return OracleSpatialRefSys
def modify_insert_params(self, placeholder, params):
"""Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
backend due to #10888.
"""
if placeholder == "NULL":
if placeholder == 'NULL':
return []
return super().modify_insert_params(placeholder, params)
@@ -236,8 +218,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
if srid:
geom.srid = srid
return geom
return converter
def get_area_att_for_field(self, field):
return "sq_m"
return 'sq_m'
@@ -4,7 +4,7 @@ from django.db.backends.utils import strip_quotes, truncate_name
class OracleGISSchemaEditor(DatabaseSchemaEditor):
sql_add_geometry_metadata = """
sql_add_geometry_metadata = ("""
INSERT INTO USER_SDO_GEOM_METADATA
("TABLE_NAME", "COLUMN_NAME", "DIMINFO", "SRID")
VALUES (
@@ -15,18 +15,13 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor):
MDSYS.SDO_DIM_ELEMENT('LAT', %(dim1)s, %(dim3)s, %(tolerance)s)
),
%(srid)s
)"""
sql_add_spatial_index = (
"CREATE INDEX %(index)s ON %(table)s(%(column)s) "
"INDEXTYPE IS MDSYS.SPATIAL_INDEX"
)
sql_drop_spatial_index = "DROP INDEX %(index)s"
sql_clear_geometry_table_metadata = (
"DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s"
)
)""")
sql_add_spatial_index = 'CREATE INDEX %(index)s ON %(table)s(%(column)s) INDEXTYPE IS MDSYS.SPATIAL_INDEX'
sql_drop_spatial_index = 'DROP INDEX %(index)s'
sql_clear_geometry_table_metadata = 'DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s'
sql_clear_geometry_field_metadata = (
"DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s "
"AND COLUMN_NAME = %(column)s"
'DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s '
'AND COLUMN_NAME = %(column)s'
)
def __init__(self, *args, **kwargs):
@@ -41,27 +36,23 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor):
if isinstance(field, GeometryField):
db_table = model._meta.db_table
self.geometry_sql.append(
self.sql_add_geometry_metadata
% {
"table": self.geo_quote_name(db_table),
"column": self.geo_quote_name(field.column),
"dim0": field._extent[0],
"dim1": field._extent[1],
"dim2": field._extent[2],
"dim3": field._extent[3],
"tolerance": field._tolerance,
"srid": field.srid,
self.sql_add_geometry_metadata % {
'table': self.geo_quote_name(db_table),
'column': self.geo_quote_name(field.column),
'dim0': field._extent[0],
'dim1': field._extent[1],
'dim2': field._extent[2],
'dim3': field._extent[3],
'tolerance': field._tolerance,
'srid': field.srid,
}
)
if field.spatial_index:
self.geometry_sql.append(
self.sql_add_spatial_index
% {
"index": self.quote_name(
self._create_spatial_index_name(model, field)
),
"table": self.quote_name(db_table),
"column": self.quote_name(field.column),
self.sql_add_spatial_index % {
'index': self.quote_name(self._create_spatial_index_name(model, field)),
'table': self.quote_name(db_table),
'column': self.quote_name(field.column),
}
)
return column_sql
@@ -72,12 +63,9 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor):
def delete_model(self, model):
super().delete_model(model)
self.execute(
self.sql_clear_geometry_table_metadata
% {
"table": self.geo_quote_name(model._meta.db_table),
}
)
self.execute(self.sql_clear_geometry_table_metadata % {
'table': self.geo_quote_name(model._meta.db_table),
})
def add_field(self, model, field):
super().add_field(model, field)
@@ -85,22 +73,14 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor):
def remove_field(self, model, field):
if isinstance(field, GeometryField):
self.execute(
self.sql_clear_geometry_field_metadata
% {
"table": self.geo_quote_name(model._meta.db_table),
"column": self.geo_quote_name(field.column),
}
)
self.execute(self.sql_clear_geometry_field_metadata % {
'table': self.geo_quote_name(model._meta.db_table),
'column': self.geo_quote_name(field.column),
})
if field.spatial_index:
self.execute(
self.sql_drop_spatial_index
% {
"index": self.quote_name(
self._create_spatial_index_name(model, field)
),
}
)
self.execute(self.sql_drop_spatial_index % {
'index': self.quote_name(self._create_spatial_index_name(model, field)),
})
super().remove_field(model, field)
def run_geometry_sql(self):
@@ -111,6 +91,4 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor):
def _create_spatial_index_name(self, model, field):
# Oracle doesn't allow object names > 30 characters. Use this scheme
# instead of self._create_index_name() for backwards compatibility.
return truncate_name(
"%s_%s_id" % (strip_quotes(model._meta.db_table), field.column), 30
)
return truncate_name('%s_%s_id' % (strip_quotes(model._meta.db_table), field.column), 30)
@@ -31,9 +31,7 @@ class PostGISAdapter:
if proto == ISQLQuote:
return self
else:
raise Exception(
"Error implementing psycopg2 protocol. Is psycopg2 installed?"
)
raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?')
def __eq__(self, other):
return isinstance(other, PostGISAdapter) and self.ewkb == other.ewkb
@@ -62,9 +60,9 @@ class PostGISAdapter:
"""
if self.is_geometry:
# Psycopg will figure out whether to use E'\\000' or '\000'.
return "%s(%s)" % (
"ST_GeogFromWKB" if self.geography else "ST_GeomFromEWKB",
self._adapter.getquoted().decode(),
return '%s(%s)' % (
'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB',
self._adapter.getquoted().decode()
)
else:
# For rasters, add explicit type cast to WKB string.
@@ -14,7 +14,7 @@ class DatabaseWrapper(Psycopg2DatabaseWrapper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if kwargs.get("alias", "") != NO_DB_ALIAS:
if kwargs.get('alias', '') != NO_DB_ALIAS:
self.features = DatabaseFeatures(self)
self.ops = PostGISOperations(self)
self.introspection = PostGISIntrospection(self)
@@ -15,23 +15,13 @@ POSTGIS_TO_GDAL = [1, 1, 1, 3, 1, 3, 2, 5, 4, None, 6, 7, None, None]
#
# Scale, origin, and skew have x and y values. PostGIS currently uses
# a fixed endianness (1) and there is only one version (0).
POSTGIS_HEADER_STRUCTURE = "B H H d d d d d d i H H"
POSTGIS_HEADER_STRUCTURE = 'B H H d d d d d d i H H'
# Lookup values to convert GDAL pixel types to struct characters. This is
# used to pack and unpack the pixel values of PostGIS raster bands.
GDAL_TO_STRUCT = [
None,
"B",
"H",
"h",
"L",
"l",
"f",
"d",
None,
None,
None,
None,
None, 'B', 'H', 'h', 'L', 'l', 'f', 'd',
None, None, None, None,
]
# Size of the packed value in bytes for different numerical types.
@@ -39,24 +29,24 @@ GDAL_TO_STRUCT = [
# when decomposing them into GDALRasters.
# See https://docs.python.org/library/struct.html#format-characters
STRUCT_SIZE = {
"b": 1, # Signed char
"B": 1, # Unsigned char
"?": 1, # _Bool
"h": 2, # Short
"H": 2, # Unsigned short
"i": 4, # Integer
"I": 4, # Unsigned Integer
"l": 4, # Long
"L": 4, # Unsigned Long
"f": 4, # Float
"d": 8, # Double
'b': 1, # Signed char
'B': 1, # Unsigned char
'?': 1, # _Bool
'h': 2, # Short
'H': 2, # Unsigned short
'i': 4, # Integer
'I': 4, # Unsigned Integer
'l': 4, # Long
'L': 4, # Unsigned Long
'f': 4, # Float
'd': 8, # Double
}
# Pixel type specifies type of pixel values in a band. Storage flag specifies
# whether the band data is stored as part of the datum or is to be found on the
# server's filesystem. There are currently 11 supported pixel value types, so 4
# bits are enough to account for all. Reserve the upper 4 bits for generic
# flags. See
# https://trac.osgeo.org/postgis/wiki/WKTRaster/RFC/RFC1_V0SerialFormat#Pixeltypeandstorageflag
# flags.
# See https://trac.osgeo.org/postgis/wiki/WKTRaster/RFC/RFC1_V0SerialFormat#Pixeltypeandstorageflag
BANDTYPE_PIXTYPE_MASK = 0x0F
BANDTYPE_FLAG_HASNODATA = 1 << 6
@@ -6,11 +6,11 @@ class PostGISIntrospection(DatabaseIntrospection):
postgis_oid_lookup = {} # Populated when introspection is performed.
ignored_tables = DatabaseIntrospection.ignored_tables + [
"geography_columns",
"geometry_columns",
"raster_columns",
"spatial_ref_sys",
"raster_overviews",
'geography_columns',
'geometry_columns',
'raster_columns',
'spatial_ref_sys',
'raster_overviews',
]
def get_field_type(self, data_type, description):
@@ -21,15 +21,9 @@ class PostGISIntrospection(DatabaseIntrospection):
# requests upon connection initialization, the `data_types_reverse`
# dictionary isn't updated until introspection is performed here.
with self.connection.cursor() as cursor:
cursor.execute(
"SELECT oid, typname "
"FROM pg_type "
"WHERE typname IN ('geometry', 'geography')"
)
cursor.execute("SELECT oid, typname FROM pg_type WHERE typname IN ('geometry', 'geography')")
self.postgis_oid_lookup = dict(cursor.fetchall())
self.data_types_reverse.update(
(oid, "GeometryField") for oid in self.postgis_oid_lookup
)
self.data_types_reverse.update((oid, 'GeometryField') for oid in self.postgis_oid_lookup)
return super().get_field_type(data_type, description)
def get_geometry_type(self, table_name, description):
@@ -40,32 +34,27 @@ class PostGISIntrospection(DatabaseIntrospection):
metadata tables to determine the geometry type.
"""
with self.connection.cursor() as cursor:
cursor.execute(
"""
cursor.execute("""
SELECT t.coord_dimension, t.srid, t.type FROM (
SELECT * FROM geometry_columns
UNION ALL
SELECT * FROM geography_columns
) AS t WHERE t.f_table_name = %s AND t.f_geometry_column = %s
""",
(table_name, description.name),
)
""", (table_name, description.name))
row = cursor.fetchone()
if not row:
raise Exception(
'Could not find a geometry or geography column for "%s"."%s"'
% (table_name, description.name)
)
raise Exception('Could not find a geometry or geography column for "%s"."%s"' %
(table_name, description.name))
dim, srid, field_type = row
# OGRGeomType does not require GDAL and makes it easy to convert
# from OGC geom type name to Django field.
field_type = OGRGeomType(field_type).django
# Getting any GeometryField keyword arguments that are not the default.
field_params = {}
if self.postgis_oid_lookup.get(description.type_code) == "geography":
field_params["geography"] = True
if self.postgis_oid_lookup.get(description.type_code) == 'geography':
field_params['geography'] = True
if srid != 4326:
field_params["srid"] = srid
field_params['srid'] = srid
if dim != 2:
field_params["dim"] = dim
field_params['dim'] = dim
return field_type, field_params
@@ -10,7 +10,6 @@ class PostGISGeometryColumns(models.Model):
The 'geometry_columns' view from PostGIS. See the PostGIS
documentation at Ch. 4.3.2.
"""
f_table_catalog = models.CharField(max_length=256)
f_table_schema = models.CharField(max_length=256)
f_table_name = models.CharField(max_length=256)
@@ -20,12 +19,12 @@ class PostGISGeometryColumns(models.Model):
type = models.CharField(max_length=30)
class Meta:
app_label = "gis"
db_table = "geometry_columns"
app_label = 'gis'
db_table = 'geometry_columns'
managed = False
def __str__(self):
return "%s.%s - %dD %s field (SRID: %d)" % (
return '%s.%s - %dD %s field (SRID: %d)' % (
self.f_table_name,
self.f_geometry_column,
self.coord_dimension,
@@ -39,7 +38,7 @@ class PostGISGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature table
name.
"""
return "f_table_name"
return 'f_table_name'
@classmethod
def geom_col_name(cls):
@@ -47,7 +46,7 @@ class PostGISGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature
geometry column.
"""
return "f_geometry_column"
return 'f_geometry_column'
class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin):
@@ -55,7 +54,6 @@ class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin):
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
documentation at Ch. 4.2.1.
"""
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(max_length=256)
auth_srid = models.IntegerField()
@@ -63,8 +61,8 @@ class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin):
proj4text = models.CharField(max_length=2048)
class Meta:
app_label = "gis"
db_table = "spatial_ref_sys"
app_label = 'gis'
db_table = 'spatial_ref_sys'
managed = False
@property
@@ -1,7 +1,9 @@
import re
from django.conf import settings
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
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
@@ -20,7 +22,7 @@ from .models import PostGISGeometryColumns, PostGISSpatialRefSys
from .pgraster import from_pgraster
# Identifier to mark raster lookups as bilateral.
BILATERAL = "bilateral"
BILATERAL = 'bilateral'
class PostGISOperator(SpatialOperator):
@@ -37,72 +39,56 @@ class PostGISOperator(SpatialOperator):
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,)
)
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"
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"
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,
)
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,
)
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"]
template_params['lhs'] = 'ST_Polygon(%s)' % template_params['lhs']
if rhs_is_raster:
template_params["rhs"] = "ST_Polygon(%s)" % template_params["rhs"]
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"]
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"]
template_params['rhs'] = 'ST_Polygon(%s)' % template_params['rhs']
return template_params
class ST_Polygon(Func):
function = "ST_Polygon"
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)
)
self.source_expressions[0] = Value(expr.value, output_field=RasterField(srid=expr.value.srid))
@cached_property
def output_field(self):
@@ -110,70 +96,64 @@ class ST_Polygon(Func):
class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
name = "postgis"
name = 'postgis'
postgis = True
geom_func_prefix = "ST_"
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"
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),
'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 = '%s::bytea'
select_extent = None
@cached_property
def function_names(self):
function_names = {
"AsWKB": "ST_AsBinary",
"AsWKT": "ST_AsText",
"BoundingCircle": "ST_MinimumBoundingCircle",
"NumPoints": "ST_NPoints",
'AsWKB': 'ST_AsBinary',
'AsWKT': 'ST_AsText',
'BoundingCircle': 'ST_MinimumBoundingCircle',
'NumPoints': 'ST_NPoints',
}
if self.spatial_version < (2, 4, 0):
function_names["ForcePolygonCW"] = "ST_ForceRHR"
function_names['ForcePolygonCW'] = 'ST_ForceRHR'
return function_names
@cached_property
@@ -185,13 +165,13 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
# 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"):
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")
self._get_postgis_func('version')
try:
vtup = self.postgis_version_tuple()
@@ -199,9 +179,9 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
raise ImproperlyConfigured(
'Cannot determine PostGIS version for database "%s" '
'using command "SELECT postgis_lib_version()". '
"GeoDjango requires at least PostGIS version 2.4. "
"Was the database created from a spatial database "
"template?" % self.connection.settings_dict["NAME"]
'GeoDjango requires at least PostGIS version 2.3. '
'Was the database created from a spatial database '
'template?' % self.connection.settings_dict['NAME']
)
version = vtup[1:]
return version
@@ -214,7 +194,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"""
if box is None:
return None
ll, ur = box[4:-1].split(",")
ll, ur = box[4:-1].split(',')
xmin, ymin = map(float, ll.split())
xmax, ymax = map(float, ur.split())
return (xmin, ymin, xmax, ymax)
@@ -227,7 +207,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"""
if box3d is None:
return None
ll, ur = box3d[6:-1].split(",")
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)
@@ -236,24 +216,22 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"""
Return the database field type for the given spatial field.
"""
if f.geom_type == "RASTER":
return "raster"
if f.geom_type == 'RASTER':
return 'raster'
# Type-based geometries.
# TODO: Support 'M' extension.
if f.dim == 3:
geom_type = f.geom_type + "Z"
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."
)
raise NotSupportedError('PostGIS only supports geography columns with an SRID of 4326.')
return "geography(%s,%d)" % (geom_type, f.srid)
return 'geography(%s,%d)' % (geom_type, f.srid)
else:
return "geometry(%s,%d)" % (geom_type, f.srid)
return 'geometry(%s,%d)' % (geom_type, f.srid)
def get_distance(self, f, dist_val, lookup_type):
"""
@@ -276,16 +254,12 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
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."
)
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))
)
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
@@ -298,12 +272,12 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
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"):
transform_func = self.spatial_function_name('Transform')
if hasattr(value, 'as_sql'):
if value.field.srid == f.srid:
placeholder = "%s"
placeholder = '%s'
else:
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
placeholder = '%s(%%s, %s)' % (transform_func, f.srid)
return placeholder
# Get the srid for this object
@@ -315,9 +289,9 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
# 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"
placeholder = '%s'
else:
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
placeholder = '%s(%%s, %s)' % (transform_func, f.srid)
return placeholder
@@ -327,28 +301,28 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"""
# Close out the connection. See #9437.
with self.connection.temporary_connection() as cursor:
cursor.execute("SELECT %s()" % func)
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")
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")
return self._get_postgis_func('postgis_lib_version')
def postgis_proj_version(self):
"""Return the version of the PROJ library used with PostGIS."""
return self._get_postgis_func("postgis_proj_version")
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")
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")
return self._get_postgis_func('postgis_full_version')
def postgis_version_tuple(self):
"""
@@ -363,16 +337,16 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
Return the version of PROJ used by PostGIS as a tuple of the
major, minor, and subminor release numbers.
"""
proj_regex = re.compile(r"(\d+)\.(\d+)\.(\d+)")
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.groups()))
else:
raise Exception("Could not determine PROJ version from PostGIS.")
raise Exception('Could not determine PROJ version from PostGIS.')
def spatial_aggregate_name(self, agg_name):
if agg_name == "Extent3D":
if agg_name == 'Extent3D':
return self.extent3d
else:
return self.geom_func_prefix + agg_name
@@ -392,15 +366,15 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
return super().distance_expr_for_lookup(
self._normalize_distance_lookup_arg(lhs),
self._normalize_distance_lookup_arg(rhs),
**kwargs,
**kwargs
)
@staticmethod
def _normalize_distance_lookup_arg(arg):
is_raster = (
arg.field.geom_type == "RASTER"
if hasattr(arg, "field")
else isinstance(arg, GDALRaster)
arg.field.geom_type == 'RASTER'
if hasattr(arg, 'field') else
isinstance(arg, GDALRaster)
)
return ST_Polygon(arg) if is_raster else arg
@@ -410,8 +384,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
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"
return 'sq_m'
@@ -3,13 +3,8 @@ import struct
from django.core.exceptions import ValidationError
from .const import (
BANDTYPE_FLAG_HASNODATA,
BANDTYPE_PIXTYPE_MASK,
GDAL_TO_POSTGIS,
GDAL_TO_STRUCT,
POSTGIS_HEADER_STRUCTURE,
POSTGIS_TO_GDAL,
STRUCT_SIZE,
BANDTYPE_FLAG_HASNODATA, BANDTYPE_PIXTYPE_MASK, GDAL_TO_POSTGIS,
GDAL_TO_STRUCT, POSTGIS_HEADER_STRUCTURE, POSTGIS_TO_GDAL, STRUCT_SIZE,
)
@@ -17,14 +12,14 @@ def pack(structure, data):
"""
Pack data into hex string with little endian format.
"""
return struct.pack("<" + structure, *data)
return struct.pack('<' + structure, *data)
def unpack(structure, data):
"""
Unpack little endian hexlified binary string into a list.
"""
return struct.unpack("<" + structure, bytes.fromhex(data))
return struct.unpack('<' + structure, bytes.fromhex(data))
def chunk(data, index):
@@ -51,7 +46,7 @@ def from_pgraster(data):
while data:
# Get pixel type for this band
pixeltype_with_flags, data = chunk(data, 2)
pixeltype_with_flags = unpack("B", pixeltype_with_flags)[0]
pixeltype_with_flags = unpack('B', pixeltype_with_flags)[0]
pixeltype = pixeltype_with_flags & BANDTYPE_PIXTYPE_MASK
# Convert datatype from PostGIS to GDAL & get pack type and size
@@ -67,11 +62,11 @@ def from_pgraster(data):
# Chunk and unpack band data (pack size times nr of pixels)
band, data = chunk(data, pack_size * header[10] * header[11])
band_result = {"data": bytes.fromhex(band)}
band_result = {'data': bytes.fromhex(band)}
# Set the nodata value if the nodata flag is set.
if pixeltype_with_flags & BANDTYPE_FLAG_HASNODATA:
band_result["nodata_value"] = nodata
band_result['nodata_value'] = nodata
# Append band data to band list
bands.append(band_result)
@@ -86,14 +81,13 @@ def from_pgraster(data):
raise ValidationError("Band pixeltypes are not all equal.")
return {
"srid": int(header[9]),
"width": header[10],
"height": header[11],
"datatype": pixeltypes[0],
"origin": (header[5], header[6]),
"scale": (header[3], header[4]),
"skew": (header[7], header[8]),
"bands": bands,
'srid': int(header[9]),
'width': header[10], 'height': header[11],
'datatype': pixeltypes[0],
'origin': (header[5], header[6]),
'scale': (header[3], header[4]),
'skew': (header[7], header[8]),
'bands': bands,
}
@@ -105,18 +99,9 @@ def to_pgraster(rast):
# the endianness and the PostGIS Raster Version, both are fixed by
# PostGIS at the moment.
rasterheader = (
1,
0,
len(rast.bands),
rast.scale.x,
rast.scale.y,
rast.origin.x,
rast.origin.y,
rast.skew.x,
rast.skew.y,
rast.srs.srid,
rast.width,
rast.height,
1, 0, len(rast.bands), rast.scale.x, rast.scale.y,
rast.origin.x, rast.origin.y, rast.skew.x, rast.skew.y,
rast.srs.srid, rast.width, rast.height,
)
# Pack raster header.
@@ -134,7 +119,7 @@ def to_pgraster(rast):
# For example, if the byte value is 71, then the datatype is
# 71 & ~BANDTYPE_FLAG_HASNODATA = 7 (32BSI)
# and the nodata value is True.
structure = "B" + GDAL_TO_STRUCT[band.datatype()]
structure = 'B' + GDAL_TO_STRUCT[band.datatype()]
# Get band pixel type in PostGIS notation
pixeltype = GDAL_TO_POSTGIS[band.datatype()]
@@ -1,60 +1,58 @@
from django.db.backends.ddl_references import Statement
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from django.db.models.expressions import Col, Func
class PostGISSchemaEditor(DatabaseSchemaEditor):
geom_index_type = "GIST"
geom_index_ops_nd = "GIST_GEOMETRY_OPS_ND"
rast_index_template = "ST_ConvexHull(%(expressions)s)"
geom_index_type = 'GIST'
geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND'
rast_index_wrapper = 'ST_ConvexHull(%s)'
sql_alter_column_to_3d = (
"ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force3D(%(column)s)::%(type)s"
)
sql_alter_column_to_2d = (
"ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force2D(%(column)s)::%(type)s"
)
sql_alter_column_to_3d = "ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force3D(%(column)s)::%(type)s"
sql_alter_column_to_2d = "ALTER COLUMN %(column)s TYPE %(type)s USING ST_Force2D(%(column)s)::%(type)s"
def geo_quote_name(self, name):
return self.connection.ops.geo_quote_name(name)
def _field_should_be_indexed(self, model, field):
if getattr(field, "spatial_index", False):
if getattr(field, 'spatial_index', False):
return True
return super()._field_should_be_indexed(model, field)
def _create_index_sql(self, model, *, fields=None, **kwargs):
if fields is None or len(fields) != 1 or not hasattr(fields[0], "geodetic"):
if fields is None or len(fields) != 1 or not hasattr(fields[0], 'geodetic'):
return super()._create_index_sql(model, fields=fields, **kwargs)
field = fields[0]
expressions = None
opclasses = None
if field.geom_type == "RASTER":
field_column = self.quote_name(field.column)
if field.geom_type == 'RASTER':
# For raster fields, wrap index creation SQL statement with ST_ConvexHull.
# Indexes on raster columns are based on the convex hull of the raster.
expressions = Func(Col(None, field), template=self.rast_index_template)
fields = None
field_column = self.rast_index_wrapper % field_column
elif field.dim > 2 and not field.geography:
# Use "nd" ops which are fast on multidimensional cases
opclasses = [self.geom_index_ops_nd]
name = kwargs.get("name")
if not name:
name = self._create_index_name(model._meta.db_table, [field.column], "_id")
field_column = "%s %s" % (field_column, self.geom_index_ops_nd)
if kwargs.get('name') is None:
index_name = '%s_%s_id' % (model._meta.db_table, field.column)
else:
index_name = kwargs['name']
return super()._create_index_sql(
model,
fields=fields,
name=name,
using=" USING %s" % self.geom_index_type,
opclasses=opclasses,
expressions=expressions,
return Statement(
self.sql_create_index,
name=self.quote_name(index_name),
table=self.quote_name(model._meta.db_table),
using=' USING %s' % self.geom_index_type,
columns=field_column,
extra='',
condition='',
include='',
)
def _alter_column_type_sql(self, table, old_field, new_field, new_type):
"""
Special case when dimension changed.
"""
if not hasattr(old_field, "dim") or not hasattr(new_field, "dim"):
if not hasattr(old_field, 'dim') or not hasattr(new_field, 'dim'):
return super()._alter_column_type_sql(table, old_field, new_field, new_type)
if old_field.dim == 2 and new_field.dim == 3:
@@ -65,8 +63,7 @@ class PostGISSchemaEditor(DatabaseSchemaEditor):
sql_alter = self.sql_alter_column_type
return (
(
sql_alter
% {
sql_alter % {
"column": self.quote_name(new_field.column),
"type": new_type,
},
@@ -4,7 +4,6 @@ from django.db.backends.sqlite3.base import Database
class SpatiaLiteAdapter(WKTAdapter):
"SQLite adapter for geometry objects."
def __conform__(self, protocol):
if protocol is Database.PrepareProtocol:
return str(self)
@@ -2,7 +2,9 @@ from ctypes.util import find_library
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.sqlite3.base import DatabaseWrapper as SQLiteDatabaseWrapper
from django.db.backends.sqlite3.base import (
DatabaseWrapper as SQLiteDatabaseWrapper,
)
from .client import SpatiaLiteClient
from .features import DatabaseFeatures
@@ -25,16 +27,12 @@ class DatabaseWrapper(SQLiteDatabaseWrapper):
# (`libspatialite`). If it's not in the system library path (e.g., it
# cannot be found by `ctypes.util.find_library`), then it may be set
# manually in the settings via the `SPATIALITE_LIBRARY_PATH` setting.
self.lib_spatialite_paths = [
name
for name in [
getattr(settings, "SPATIALITE_LIBRARY_PATH", None),
"mod_spatialite.so",
"mod_spatialite",
find_library("spatialite"),
]
if name is not None
]
self.lib_spatialite_paths = [name for name in [
getattr(settings, 'SPATIALITE_LIBRARY_PATH', None),
'mod_spatialite.so',
'mod_spatialite',
find_library('spatialite'),
] if name is not None]
super().__init__(*args, **kwargs)
def get_new_connection(self, conn_params):
@@ -44,26 +42,26 @@ class DatabaseWrapper(SQLiteDatabaseWrapper):
conn.enable_load_extension(True)
except AttributeError:
raise ImproperlyConfigured(
"SpatiaLite requires SQLite to be configured to allow "
"extension loading."
'SpatiaLite requires SQLite to be configured to allow '
'extension loading.'
)
# Load the SpatiaLite library extension on the connection.
for path in self.lib_spatialite_paths:
try:
conn.load_extension(path)
except Exception:
if getattr(settings, "SPATIALITE_LIBRARY_PATH", None):
if getattr(settings, 'SPATIALITE_LIBRARY_PATH', None):
raise ImproperlyConfigured(
"Unable to load the SpatiaLite library extension "
"as specified in your SPATIALITE_LIBRARY_PATH setting."
'Unable to load the SpatiaLite library extension '
'as specified in your SPATIALITE_LIBRARY_PATH setting.'
)
continue
else:
break
else:
raise ImproperlyConfigured(
"Unable to load the SpatiaLite library extension. "
"Library names tried: %s" % ", ".join(self.lib_spatialite_paths)
'Unable to load the SpatiaLite library extension. '
'Library names tried: %s' % ', '.join(self.lib_spatialite_paths)
)
return conn
@@ -73,7 +71,4 @@ class DatabaseWrapper(SQLiteDatabaseWrapper):
with self.cursor() as cursor:
cursor.execute("PRAGMA table_info(geometry_columns);")
if cursor.fetchall() == []:
if self.ops.spatial_version < (5,):
cursor.execute("SELECT InitSpatialMetaData(1)")
else:
cursor.execute("SELECT InitSpatialMetaDataFull(1)")
cursor.execute("SELECT InitSpatialMetaData(1)")
@@ -2,4 +2,4 @@ from django.db.backends.sqlite3.client import DatabaseClient
class SpatiaLiteClient(DatabaseClient):
executable_name = "spatialite"
executable_name = 'spatialite'
@@ -11,16 +11,14 @@ class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures):
@cached_property
def supports_area_geodetic(self):
return bool(self.connection.ops.geom_lib_version())
return bool(self.connection.ops.lwgeom_version())
@cached_property
def django_test_skips(self):
skips = super().django_test_skips
skips.update(
{
"SpatiaLite doesn't support distance lookups with Distance objects.": {
"gis_tests.geogapp.tests.GeographyTest.test02_distance_lookup",
},
}
)
skips.update({
"SpatiaLite doesn't support distance lookups with Distance objects.": {
'gis_tests.geogapp.tests.GeographyTest.test02_distance_lookup',
},
})
return skips
@@ -1,7 +1,6 @@
from django.contrib.gis.gdal import OGRGeomType
from django.db.backends.sqlite3.introspection import (
DatabaseIntrospection,
FlexibleFieldLookupDict,
DatabaseIntrospection, FlexibleFieldLookupDict,
)
@@ -10,16 +9,15 @@ class GeoFlexibleFieldLookupDict(FlexibleFieldLookupDict):
Subclass that includes updates the `base_data_types_reverse` dict
for geometry field types.
"""
base_data_types_reverse = {
**FlexibleFieldLookupDict.base_data_types_reverse,
"point": "GeometryField",
"linestring": "GeometryField",
"polygon": "GeometryField",
"multipoint": "GeometryField",
"multilinestring": "GeometryField",
"multipolygon": "GeometryField",
"geometrycollection": "GeometryField",
'point': 'GeometryField',
'linestring': 'GeometryField',
'polygon': 'GeometryField',
'multipoint': 'GeometryField',
'multilinestring': 'GeometryField',
'multipolygon': 'GeometryField',
'geometrycollection': 'GeometryField',
}
@@ -29,18 +27,14 @@ class SpatiaLiteIntrospection(DatabaseIntrospection):
def get_geometry_type(self, table_name, description):
with self.connection.cursor() as cursor:
# Querying the `geometry_columns` table to get additional metadata.
cursor.execute(
"SELECT coord_dimension, srid, geometry_type "
"FROM geometry_columns "
"WHERE f_table_name=%s AND f_geometry_column=%s",
(table_name, description.name),
)
cursor.execute('SELECT coord_dimension, srid, geometry_type '
'FROM geometry_columns '
'WHERE f_table_name=%s AND f_geometry_column=%s',
(table_name, description.name))
row = cursor.fetchone()
if not row:
raise Exception(
'Could not find a geometry column for "%s"."%s"'
% (table_name, description.name)
)
raise Exception('Could not find a geometry column for "%s"."%s"' %
(table_name, description.name))
# OGRGeomType does not require GDAL and makes it easy to convert
# from OGC geom type name to Django field.
@@ -57,21 +51,18 @@ class SpatiaLiteIntrospection(DatabaseIntrospection):
srid = row[1]
field_params = {}
if srid != 4326:
field_params["srid"] = srid
if (isinstance(dim, str) and "Z" in dim) or dim == 3:
field_params["dim"] = 3
field_params['srid'] = srid
if (isinstance(dim, str) and 'Z' in dim) or dim == 3:
field_params['dim'] = 3
return field_type, field_params
def get_constraints(self, cursor, table_name):
constraints = super().get_constraints(cursor, table_name)
cursor.execute(
"SELECT f_geometry_column "
"FROM geometry_columns "
"WHERE f_table_name=%s AND spatial_index_enabled=1",
(table_name,),
)
cursor.execute('SELECT f_geometry_column '
'FROM geometry_columns '
'WHERE f_table_name=%s AND spatial_index_enabled=1', (table_name,))
for row in cursor.fetchall():
constraints["%s__spatial__index" % row[0]] = {
constraints['%s__spatial__index' % row[0]] = {
"columns": [row[0]],
"primary_key": False,
"unique": False,
@@ -9,21 +9,20 @@ class SpatialiteGeometryColumns(models.Model):
"""
The 'geometry_columns' table from SpatiaLite.
"""
f_table_name = models.CharField(max_length=256)
f_geometry_column = models.CharField(max_length=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField(primary_key=True)
spatial_index_enabled = models.IntegerField()
type = models.IntegerField(db_column="geometry_type")
type = models.IntegerField(db_column='geometry_type')
class Meta:
app_label = "gis"
db_table = "geometry_columns"
app_label = 'gis'
db_table = 'geometry_columns'
managed = False
def __str__(self):
return "%s.%s - %dD %s field (SRID: %d)" % (
return '%s.%s - %dD %s field (SRID: %d)' % (
self.f_table_name,
self.f_geometry_column,
self.coord_dimension,
@@ -37,7 +36,7 @@ class SpatialiteGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature table
name.
"""
return "f_table_name"
return 'f_table_name'
@classmethod
def geom_col_name(cls):
@@ -45,14 +44,13 @@ class SpatialiteGeometryColumns(models.Model):
Return the name of the metadata column used to store the feature
geometry column.
"""
return "f_geometry_column"
return 'f_geometry_column'
class SpatialiteSpatialRefSys(models.Model, SpatialRefSysMixin):
"""
The 'spatial_ref_sys' table from SpatiaLite.
"""
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(max_length=256)
auth_srid = models.IntegerField()
@@ -61,8 +59,8 @@ class SpatialiteSpatialRefSys(models.Model, SpatialRefSysMixin):
srtext = models.CharField(max_length=2048)
class Meta:
app_label = "gis"
db_table = "spatial_ref_sys"
app_label = 'gis'
db_table = 'spatial_ref_sys'
managed = False
@property
@@ -3,7 +3,9 @@ SQL functions reference lists:
https://www.gaia-gis.it/gaia-sins/spatialite-sql-4.3.0.html
"""
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.db.backends.base.operations import (
BaseSpatialOperations,
)
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
@@ -18,69 +20,69 @@ from django.utils.version import get_version_tuple
class SpatialiteNullCheckOperator(SpatialOperator):
def as_sql(self, connection, lookup, template_params, sql_params):
sql, params = super().as_sql(connection, lookup, template_params, sql_params)
return "%s > 0" % sql, params
return '%s > 0' % sql, params
class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
name = "spatialite"
name = 'spatialite'
spatialite = True
Adapter = SpatiaLiteAdapter
collect = "Collect"
extent = "Extent"
makeline = "MakeLine"
unionagg = "GUnion"
collect = 'Collect'
extent = 'Extent'
makeline = 'MakeLine'
unionagg = 'GUnion'
from_text = "GeomFromText"
from_text = 'GeomFromText'
gis_operators = {
# Binary predicates
"equals": SpatialiteNullCheckOperator(func="Equals"),
"disjoint": SpatialiteNullCheckOperator(func="Disjoint"),
"touches": SpatialiteNullCheckOperator(func="Touches"),
"crosses": SpatialiteNullCheckOperator(func="Crosses"),
"within": SpatialiteNullCheckOperator(func="Within"),
"overlaps": SpatialiteNullCheckOperator(func="Overlaps"),
"contains": SpatialiteNullCheckOperator(func="Contains"),
"intersects": SpatialiteNullCheckOperator(func="Intersects"),
"relate": SpatialiteNullCheckOperator(func="Relate"),
"coveredby": SpatialiteNullCheckOperator(func="CoveredBy"),
"covers": SpatialiteNullCheckOperator(func="Covers"),
'equals': SpatialiteNullCheckOperator(func='Equals'),
'disjoint': SpatialiteNullCheckOperator(func='Disjoint'),
'touches': SpatialiteNullCheckOperator(func='Touches'),
'crosses': SpatialiteNullCheckOperator(func='Crosses'),
'within': SpatialiteNullCheckOperator(func='Within'),
'overlaps': SpatialiteNullCheckOperator(func='Overlaps'),
'contains': SpatialiteNullCheckOperator(func='Contains'),
'intersects': SpatialiteNullCheckOperator(func='Intersects'),
'relate': SpatialiteNullCheckOperator(func='Relate'),
'coveredby': SpatialiteNullCheckOperator(func='CoveredBy'),
'covers': SpatialiteNullCheckOperator(func='Covers'),
# Returns true if B's bounding box completely contains A's bounding box.
"contained": SpatialOperator(func="MbrWithin"),
'contained': SpatialOperator(func='MbrWithin'),
# Returns true if A's bounding box completely contains B's bounding box.
"bbcontains": SpatialOperator(func="MbrContains"),
'bbcontains': SpatialOperator(func='MbrContains'),
# Returns true if A's bounding box overlaps B's bounding box.
"bboverlaps": SpatialOperator(func="MbrOverlaps"),
'bboverlaps': SpatialOperator(func='MbrOverlaps'),
# These are implemented here as synonyms for Equals
"same_as": SpatialiteNullCheckOperator(func="Equals"),
"exact": SpatialiteNullCheckOperator(func="Equals"),
'same_as': SpatialiteNullCheckOperator(func='Equals'),
'exact': SpatialiteNullCheckOperator(func='Equals'),
# Distance predicates
"dwithin": SpatialOperator(func="PtDistWithin"),
'dwithin': SpatialOperator(func='PtDistWithin'),
}
disallowed_aggregates = (models.Extent3D,)
select = "CAST (AsEWKB(%s) AS BLOB)"
select = 'CAST (AsEWKB(%s) AS BLOB)'
function_names = {
"AsWKB": "St_AsBinary",
"ForcePolygonCW": "ST_ForceLHR",
"Length": "ST_Length",
"LineLocatePoint": "ST_Line_Locate_Point",
"NumPoints": "ST_NPoints",
"Reverse": "ST_Reverse",
"Scale": "ScaleCoords",
"Translate": "ST_Translate",
"Union": "ST_Union",
'AsWKB': 'St_AsBinary',
'ForcePolygonCW': 'ST_ForceLHR',
'Length': 'ST_Length',
'LineLocatePoint': 'ST_Line_Locate_Point',
'NumPoints': 'ST_NPoints',
'Reverse': 'ST_Reverse',
'Scale': 'ScaleCoords',
'Translate': 'ST_Translate',
'Union': 'ST_Union',
}
@cached_property
def unsupported_functions(self):
unsupported = {"BoundingCircle", "GeometryDistance", "MemSize"}
if not self.geom_lib_version():
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
unsupported = {'BoundingCircle', 'GeometryDistance', 'MemSize'}
if not self.lwgeom_version():
unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'}
return unsupported
@cached_property
@@ -91,11 +93,12 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
except Exception as exc:
raise ImproperlyConfigured(
'Cannot determine the SpatiaLite version for the "%s" database. '
"Was the SpatiaLite initialization SQL loaded on this database?"
% (self.connection.settings_dict["NAME"],)
'Was the SpatiaLite initialization SQL loaded on this database?' % (
self.connection.settings_dict['NAME'],
)
) from exc
if version < (4, 3, 0):
raise ImproperlyConfigured("GeoDjango supports SpatiaLite 4.3.0 and above.")
raise ImproperlyConfigured('GeoDjango supports SpatiaLite 4.3.0 and above.')
return version
def convert_extent(self, box):
@@ -126,16 +129,14 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
value = value[0]
if isinstance(value, Distance):
if f.geodetic(self.connection):
if lookup_type == "dwithin":
if lookup_type == 'dwithin':
raise ValueError(
"Only numeric values of degree units are allowed on "
"geographic DWithin queries."
'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))
)
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
else:
dist_param = value
return [dist_param]
@@ -148,7 +149,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
"""
cursor = self.connection._cursor()
try:
cursor.execute("SELECT %s" % func)
cursor.execute('SELECT %s' % func)
row = cursor.fetchone()
finally:
cursor.close()
@@ -156,33 +157,19 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
def geos_version(self):
"Return the version of GEOS used by SpatiaLite as a string."
return self._get_spatialite_func("geos_version()")
return self._get_spatialite_func('geos_version()')
def proj_version(self):
"""Return the version of the PROJ library used by SpatiaLite."""
return self._get_spatialite_func("proj4_version()")
return self._get_spatialite_func('proj4_version()')
def lwgeom_version(self):
"""Return the version of LWGEOM library used by SpatiaLite."""
return self._get_spatialite_func("lwgeom_version()")
def rttopo_version(self):
"""Return the version of RTTOPO library used by SpatiaLite."""
return self._get_spatialite_func("rttopo_version()")
def geom_lib_version(self):
"""
Return the version of the version-dependant geom library used by
SpatiaLite.
"""
if self.spatial_version >= (5,):
return self.rttopo_version()
else:
return self.lwgeom_version()
return self._get_spatialite_func('lwgeom_version()')
def spatialite_version(self):
"Return the SpatiaLite library version as a string."
return self._get_spatialite_func("spatialite_version()")
return self._get_spatialite_func('spatialite_version()')
def spatialite_version_tuple(self):
"""
@@ -197,7 +184,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
Return the spatial aggregate SQL template and function for the
given Aggregate instance.
"""
agg_name = "unionagg" if agg_name.lower() == "union" else agg_name.lower()
agg_name = 'unionagg' if agg_name.lower() == 'union' else agg_name.lower()
return getattr(self, agg_name)
# Routines for getting the OGC-compliant models.
@@ -205,14 +192,12 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
from django.contrib.gis.db.backends.spatialite.models import (
SpatialiteGeometryColumns,
)
return SpatialiteGeometryColumns
def spatial_ref_sys(self):
from django.contrib.gis.db.backends.spatialite.models import (
SpatialiteSpatialRefSys,
)
return SpatialiteSpatialRefSys
def get_geometry_converter(self, expression):
@@ -221,5 +206,4 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
def converter(value, expression, connection):
return None if value is None else GEOSGeometryBase(read(value), geom_class)
return converter
@@ -14,9 +14,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
"%(geom_type)s, %(dim)s)"
)
sql_remove_geometry_metadata = "SELECT DiscardGeometryColumn(%(table)s, %(column)s)"
sql_discard_geometry_columns = (
"DELETE FROM %(geom_table)s WHERE f_table_name = %(table)s"
)
sql_discard_geometry_columns = "DELETE FROM %(geom_table)s WHERE f_table_name = %(table)s"
sql_update_geometry_columns = (
"UPDATE %(geom_table)s SET f_table_name = %(new_table)s "
"WHERE f_table_name = %(old_table)s"
@@ -38,14 +36,12 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
def column_sql(self, model, field, include_default=False):
from django.contrib.gis.db.models import GeometryField
if not isinstance(field, GeometryField):
return super().column_sql(model, field, include_default)
# Geometry columns are created by the `AddGeometryColumn` function
self.geometry_sql.append(
self.sql_add_geometry_column
% {
self.sql_add_geometry_column % {
"table": self.geo_quote_name(model._meta.db_table),
"column": self.geo_quote_name(field.column),
"srid": field.srid,
@@ -57,8 +53,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
if field.spatial_index:
self.geometry_sql.append(
self.sql_add_spatial_index
% {
self.sql_add_spatial_index % {
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(field.column),
}
@@ -67,15 +62,13 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
def remove_geometry_metadata(self, model, field):
self.execute(
self.sql_remove_geometry_metadata
% {
self.sql_remove_geometry_metadata % {
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(field.column),
}
)
self.execute(
self.sql_drop_spatial_index
% {
self.sql_drop_spatial_index % {
"table": model._meta.db_table,
"column": field.column,
}
@@ -99,8 +92,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
for geom_table in self.geometry_tables:
try:
self.execute(
self.sql_discard_geometry_columns
% {
self.sql_discard_geometry_columns % {
"geom_table": geom_table,
"table": self.quote_name(model._meta.db_table),
}
@@ -111,7 +103,6 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
def add_field(self, model, field):
from django.contrib.gis.db.models import GeometryField
if isinstance(field, GeometryField):
# Populate self.geometry_sql
self.column_sql(model, field)
@@ -134,17 +125,14 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
else:
super().remove_field(model, field)
def alter_db_table(
self, model, old_db_table, new_db_table, disable_constraints=True
):
def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True):
from django.contrib.gis.db.models import GeometryField
# Remove geometry-ness from temp table
for field in model._meta.local_fields:
if isinstance(field, GeometryField):
self.execute(
self.sql_remove_geometry_metadata
% {
self.sql_remove_geometry_metadata % {
"table": self.quote_name(old_db_table),
"column": self.quote_name(field.column),
}
@@ -155,8 +143,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
for geom_table in self.geometry_tables:
try:
self.execute(
self.sql_update_geometry_columns
% {
self.sql_update_geometry_columns % {
"geom_table": geom_table,
"old_table": self.quote_name(old_db_table),
"new_table": self.quote_name(new_db_table),
@@ -167,25 +154,15 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
# Re-add geometry-ness and rename spatial index tables
for field in model._meta.local_fields:
if isinstance(field, GeometryField):
self.execute(
self.sql_recover_geometry_metadata
% {
"table": self.geo_quote_name(new_db_table),
"column": self.geo_quote_name(field.column),
"srid": field.srid,
"geom_type": self.geo_quote_name(field.geom_type),
"dim": field.dim,
}
)
if getattr(field, "spatial_index", False):
self.execute(
self.sql_rename_table
% {
"old_table": self.quote_name(
"idx_%s_%s" % (old_db_table, field.column)
),
"new_table": self.quote_name(
"idx_%s_%s" % (new_db_table, field.column)
),
}
)
self.execute(self.sql_recover_geometry_metadata % {
"table": self.geo_quote_name(new_db_table),
"column": self.geo_quote_name(field.column),
"srid": field.srid,
"geom_type": self.geo_quote_name(field.geom_type),
"dim": field.dim,
})
if getattr(field, 'spatial_index', False):
self.execute(self.sql_rename_table % {
"old_table": self.quote_name("idx_%s_%s" % (old_db_table, field.column)),
"new_table": self.quote_name("idx_%s_%s" % (new_db_table, field.column)),
})
@@ -8,7 +8,6 @@ class SpatialOperator:
"""
Class encapsulating the behavior specific to a GIS operation (used by lookups).
"""
sql_template = None
def __init__(self, op=None, func=None):
@@ -18,11 +17,11 @@ class SpatialOperator:
@property
def default_template(self):
if self.func:
return "%(func)s(%(lhs)s, %(rhs)s)"
return '%(func)s(%(lhs)s, %(rhs)s)'
else:
return "%(lhs)s %(op)s %(rhs)s"
return '%(lhs)s %(op)s %(rhs)s'
def as_sql(self, connection, lookup, template_params, sql_params):
sql_template = self.sql_template or lookup.sql_template or self.default_template
template_params.update({"op": self.op, "func": self.func})
template_params.update({'op': self.op, 'func': self.func})
return sql_template % template_params, sql_params
@@ -5,26 +5,14 @@ import django.contrib.gis.db.models.lookups # NOQA
from django.contrib.gis.db.models.aggregates import * # NOQA
from django.contrib.gis.db.models.aggregates import __all__ as aggregates_all
from django.contrib.gis.db.models.fields import (
GeometryCollectionField,
GeometryField,
LineStringField,
MultiLineStringField,
MultiPointField,
MultiPolygonField,
PointField,
PolygonField,
RasterField,
GeometryCollectionField, GeometryField, LineStringField,
MultiLineStringField, MultiPointField, MultiPolygonField, PointField,
PolygonField, RasterField,
)
__all__ = models_all + aggregates_all
__all__ += [
"GeometryCollectionField",
"GeometryField",
"LineStringField",
"MultiLineStringField",
"MultiPointField",
"MultiPolygonField",
"PointField",
"PolygonField",
"RasterField",
'GeometryCollectionField', 'GeometryField', 'LineStringField',
'MultiLineStringField', 'MultiPointField', 'MultiPolygonField', 'PointField',
'PolygonField', 'RasterField',
]
@@ -1,13 +1,10 @@
from django.contrib.gis.db.models.fields import (
ExtentField,
GeometryCollectionField,
GeometryField,
LineStringField,
ExtentField, GeometryCollectionField, GeometryField, LineStringField,
)
from django.db.models import Aggregate, Value
from django.utils.functional import cached_property
__all__ = ["Collect", "Extent", "Extent3D", "MakeLine", "Union"]
__all__ = ['Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union']
class GeoAggregate(Aggregate):
@@ -26,45 +23,37 @@ class GeoAggregate(Aggregate):
compiler,
connection,
function=function or connection.ops.spatial_aggregate_name(self.name),
**extra_context,
**extra_context
)
def as_oracle(self, compiler, connection, **extra_context):
if not self.is_extent:
tolerance = self.extra.get("tolerance") or getattr(self, "tolerance", 0.05)
tolerance = self.extra.get('tolerance') or getattr(self, 'tolerance', 0.05)
clone = self.copy()
clone.set_source_expressions(
[
*self.get_source_expressions(),
Value(tolerance),
]
)
template = "%(function)s(SDOAGGRTYPE(%(expressions)s))"
return clone.as_sql(
compiler, connection, template=template, **extra_context
)
clone.set_source_expressions([
*self.get_source_expressions(),
Value(tolerance),
])
template = '%(function)s(SDOAGGRTYPE(%(expressions)s))'
return clone.as_sql(compiler, connection, template=template, **extra_context)
return self.as_sql(compiler, connection, **extra_context)
def resolve_expression(
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
for expr in c.get_source_expressions():
if not hasattr(expr.field, "geom_type"):
raise ValueError(
"Geospatial aggregates only allowed on geometry fields."
)
if not hasattr(expr.field, 'geom_type'):
raise ValueError('Geospatial aggregates only allowed on geometry fields.')
return c
class Collect(GeoAggregate):
name = "Collect"
name = 'Collect'
output_field_class = GeometryCollectionField
class Extent(GeoAggregate):
name = "Extent"
is_extent = "2D"
name = 'Extent'
is_extent = '2D'
def __init__(self, expression, **extra):
super().__init__(expression, output_field=ExtentField(), **extra)
@@ -74,8 +63,8 @@ class Extent(GeoAggregate):
class Extent3D(GeoAggregate):
name = "Extent3D"
is_extent = "3D"
name = 'Extent3D'
is_extent = '3D'
def __init__(self, expression, **extra):
super().__init__(expression, output_field=ExtentField(), **extra)
@@ -85,10 +74,10 @@ class Extent3D(GeoAggregate):
class MakeLine(GeoAggregate):
name = "MakeLine"
name = 'MakeLine'
output_field_class = LineStringField
class Union(GeoAggregate):
name = "Union"
name = 'Union'
output_field_class = GeometryField
@@ -4,15 +4,8 @@ from django.contrib.gis import forms, gdal
from django.contrib.gis.db.models.proxy import SpatialProxy
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.geos import (
GeometryCollection,
GEOSException,
GEOSGeometry,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
GeometryCollection, GEOSException, GEOSGeometry, LineString,
MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
)
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Field
@@ -24,9 +17,7 @@ from django.utils.translation import gettext_lazy as _
_srid_cache = defaultdict(dict)
SRIDCacheEntry = namedtuple(
"SRIDCacheEntry", ["units", "units_name", "spheroid", "geodetic"]
)
SRIDCacheEntry = namedtuple('SRIDCacheEntry', ['units', 'units_name', 'spheroid', 'geodetic'])
def get_srid_info(srid, connection):
@@ -36,7 +27,6 @@ def get_srid_info(srid, connection):
table for the given database connection. These results are cached.
"""
from django.contrib.gis.gdal import SpatialReference
global _srid_cache
try:
@@ -46,14 +36,9 @@ def get_srid_info(srid, connection):
SpatialRefSys = None
alias, get_srs = (
(
connection.alias,
lambda srid: SpatialRefSys.objects.using(connection.alias)
.get(srid=srid)
.srs,
)
if SpatialRefSys
else (None, SpatialReference)
(connection.alias, lambda srid: SpatialRefSys.objects.using(connection.alias).get(srid=srid).srs)
if SpatialRefSys else
(None, SpatialReference)
)
if srid not in _srid_cache[alias]:
srs = get_srs(srid)
@@ -61,8 +46,7 @@ def get_srid_info(srid, connection):
_srid_cache[alias][srid] = SRIDCacheEntry(
units=units,
units_name=units_name,
spheroid='SPHEROID["%s",%s,%s]'
% (srs["spheroid"], srs.semi_major, srs.inverse_flattening),
spheroid='SPHEROID["%s",%s,%s]' % (srs['spheroid'], srs.semi_major, srs.inverse_flattening),
geodetic=srs.geographic,
)
@@ -77,7 +61,6 @@ class BaseSpatialField(Field):
properties that are common to all GIS fields such as the characteristics
of the spatial reference system of the field.
"""
description = _("The base GIS field.")
empty_strings_allowed = False
@@ -105,7 +88,7 @@ class BaseSpatialField(Field):
# Setting the verbose_name keyword argument with the positional
# first parameter, so this works like normal fields.
kwargs["verbose_name"] = verbose_name
kwargs['verbose_name'] = verbose_name
super().__init__(**kwargs)
@@ -113,9 +96,9 @@ class BaseSpatialField(Field):
name, path, args, kwargs = super().deconstruct()
# Always include SRID for less fragility; include spatial index if it's
# not the default value.
kwargs["srid"] = self.srid
kwargs['srid'] = self.srid
if self.spatial_index is not True:
kwargs["spatial_index"] = self.spatial_index
kwargs['spatial_index'] = self.spatial_index
return name, path, args, kwargs
def db_type(self, connection):
@@ -163,10 +146,10 @@ class BaseSpatialField(Field):
return connection.ops.Adapter(
super().get_db_prep_value(value, connection, *args, **kwargs),
**(
{"geography": True}
{'geography': True}
if self.geography and connection.features.supports_geography
else {}
),
)
)
def get_raster_prep_value(self, value, is_candidate):
@@ -184,9 +167,7 @@ class BaseSpatialField(Field):
try:
return gdal.GDALRaster(value)
except GDALException:
raise ValueError(
"Couldn't create spatial object from lookup value '%s'." % value
)
raise ValueError("Couldn't create spatial object from lookup value '%s'." % value)
def get_prep_value(self, value):
obj = super().get_prep_value(value)
@@ -198,9 +179,7 @@ class BaseSpatialField(Field):
pass
else:
# Check if input is a candidate for conversion to raster or geometry.
is_candidate = isinstance(obj, (bytes, str)) or hasattr(
obj, "__geo_interface__"
)
is_candidate = isinstance(obj, (bytes, str)) or hasattr(obj, '__geo_interface__')
# Try to convert the input to raster.
raster = self.get_raster_prep_value(obj, is_candidate)
@@ -210,14 +189,9 @@ class BaseSpatialField(Field):
try:
obj = GEOSGeometry(obj)
except (GEOSException, GDALException):
raise ValueError(
"Couldn't create spatial object from lookup value '%s'." % obj
)
raise ValueError("Couldn't create spatial object from lookup value '%s'." % obj)
else:
raise ValueError(
"Cannot use object with type %s for a spatial lookup parameter."
% type(obj).__name__
)
raise ValueError('Cannot use object with type %s for a spatial lookup parameter.' % type(obj).__name__)
# Assigning the SRID value.
obj.srid = self.get_srid(obj)
@@ -228,25 +202,14 @@ class GeometryField(BaseSpatialField):
"""
The base Geometry field -- maps to the OpenGIS Specification Geometry type.
"""
description = _(
"The base Geometry field — maps to the OpenGIS Specification Geometry type."
)
description = _('The base Geometry field — maps to the OpenGIS Specification Geometry type.')
form_class = forms.GeometryField
# The OpenGIS Geometry name.
geom_type = "GEOMETRY"
geom_type = 'GEOMETRY'
geom_class = None
def __init__(
self,
verbose_name=None,
dim=2,
geography=False,
*,
extent=(-180.0, -90.0, 180.0, 90.0),
tolerance=0.05,
**kwargs,
):
def __init__(self, verbose_name=None, dim=2, geography=False, *, extent=(-180.0, -90.0, 180.0, 90.0),
tolerance=0.05, **kwargs):
"""
The initialization function for geometry fields. In addition to the
parameters from BaseSpatialField, it takes the following as keyword
@@ -281,36 +244,30 @@ class GeometryField(BaseSpatialField):
name, path, args, kwargs = super().deconstruct()
# Include kwargs if they're not the default values.
if self.dim != 2:
kwargs["dim"] = self.dim
kwargs['dim'] = self.dim
if self.geography is not False:
kwargs["geography"] = self.geography
kwargs['geography'] = self.geography
if self._extent != (-180.0, -90.0, 180.0, 90.0):
kwargs["extent"] = self._extent
kwargs['extent'] = self._extent
if self._tolerance != 0.05:
kwargs["tolerance"] = self._tolerance
kwargs['tolerance'] = self._tolerance
return name, path, args, kwargs
def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs)
# Setup for lazy-instantiated Geometry object.
setattr(
cls,
self.attname,
SpatialProxy(self.geom_class or GEOSGeometry, self, load_func=GEOSGeometry),
)
setattr(cls, self.attname, SpatialProxy(self.geom_class or GEOSGeometry, self, load_func=GEOSGeometry))
def formfield(self, **kwargs):
defaults = {
"form_class": self.form_class,
"geom_type": self.geom_type,
"srid": self.srid,
'form_class': self.form_class,
'geom_type': self.geom_type,
'srid': self.srid,
**kwargs,
}
if self.dim > 2 and not getattr(
defaults["form_class"].widget, "supports_3d", False
):
defaults.setdefault("widget", forms.Textarea)
if self.dim > 2 and not getattr(defaults['form_class'].widget, 'supports_3d', False):
defaults.setdefault('widget', forms.Textarea)
return super().formfield(**defaults)
def select_format(self, compiler, sql, params):
@@ -326,49 +283,49 @@ class GeometryField(BaseSpatialField):
# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
geom_type = "POINT"
geom_type = 'POINT'
geom_class = Point
form_class = forms.PointField
description = _("Point")
class LineStringField(GeometryField):
geom_type = "LINESTRING"
geom_type = 'LINESTRING'
geom_class = LineString
form_class = forms.LineStringField
description = _("Line string")
class PolygonField(GeometryField):
geom_type = "POLYGON"
geom_type = 'POLYGON'
geom_class = Polygon
form_class = forms.PolygonField
description = _("Polygon")
class MultiPointField(GeometryField):
geom_type = "MULTIPOINT"
geom_type = 'MULTIPOINT'
geom_class = MultiPoint
form_class = forms.MultiPointField
description = _("Multi-point")
class MultiLineStringField(GeometryField):
geom_type = "MULTILINESTRING"
geom_type = 'MULTILINESTRING'
geom_class = MultiLineString
form_class = forms.MultiLineStringField
description = _("Multi-line string")
class MultiPolygonField(GeometryField):
geom_type = "MULTIPOLYGON"
geom_type = 'MULTIPOLYGON'
geom_class = MultiPolygon
form_class = forms.MultiPolygonField
description = _("Multi polygon")
class GeometryCollectionField(GeometryField):
geom_type = "GEOMETRYCOLLECTION"
geom_type = 'GEOMETRYCOLLECTION'
geom_class = GeometryCollection
form_class = forms.GeometryCollectionField
description = _("Geometry collection")
@@ -393,18 +350,13 @@ class RasterField(BaseSpatialField):
"""
description = _("Raster Field")
geom_type = "RASTER"
geom_type = 'RASTER'
geography = False
def _check_connection(self, connection):
# Make sure raster fields are used only on backends with raster support.
if (
not connection.features.gis_enabled
or not connection.features.supports_raster
):
raise ImproperlyConfigured(
"Raster fields require backends with raster support."
)
if not connection.features.gis_enabled or not connection.features.supports_raster:
raise ImproperlyConfigured('Raster fields require backends with raster support.')
def db_type(self, connection):
self._check_connection(connection)
@@ -423,13 +375,12 @@ class RasterField(BaseSpatialField):
def get_transform(self, name):
from django.contrib.gis.db.models.lookups import RasterBandTransform
try:
band_index = int(name)
return type(
"SpecificRasterBandTransform",
'SpecificRasterBandTransform',
(RasterBandTransform,),
{"band_index": band_index},
{'band_index': band_index}
)
except ValueError:
pass
@@ -6,14 +6,8 @@ from django.contrib.gis.geos import GEOSGeometry
from django.core.exceptions import FieldError
from django.db import NotSupportedError
from django.db.models import (
BinaryField,
BooleanField,
FloatField,
Func,
IntegerField,
TextField,
Transform,
Value,
BinaryField, BooleanField, FloatField, Func, IntegerField, TextField,
Transform, Value,
)
from django.db.models.functions import Cast
from django.utils.functional import cached_property
@@ -38,21 +32,12 @@ class GeoFuncMixin:
except FieldError:
output_field = None
geom = expr.value
if (
not isinstance(geom, GEOSGeometry)
or output_field
and not isinstance(output_field, GeometryField)
):
raise TypeError(
"%s function requires a geometric argument in position %d."
% (self.name, pos + 1)
)
if not isinstance(geom, GEOSGeometry) or output_field and not isinstance(output_field, GeometryField):
raise TypeError("%s function requires a geometric argument in position %d." % (self.name, pos + 1))
if not geom.srid and not output_field:
raise ValueError("SRID is required for all geometries.")
if not output_field:
self.source_expressions[pos] = Value(
geom, output_field=GeometryField(srid=geom.srid)
)
self.source_expressions[pos] = Value(geom, output_field=GeometryField(srid=geom.srid))
@property
def name(self):
@@ -76,11 +61,8 @@ class GeoFuncMixin:
field = source_fields[pos]
if not isinstance(field, GeometryField):
raise TypeError(
"%s function requires a GeometryField in position %s, got %s."
% (
self.name,
pos + 1,
type(field).__name__,
"%s function requires a GeometryField in position %s, got %s." % (
self.name, pos + 1, type(field).__name__,
)
)
@@ -90,17 +72,15 @@ class GeoFuncMixin:
expr_srid = expr.output_field.srid
if expr_srid != base_srid:
# Automatic SRID conversion so objects are comparable.
res.source_expressions[pos] = Transform(
expr, base_srid
).resolve_expression(*args, **kwargs)
res.source_expressions[pos] = Transform(expr, base_srid).resolve_expression(*args, **kwargs)
return res
def _handle_param(self, value, param_name="", check_types=None):
if not hasattr(value, "resolve_expression"):
def _handle_param(self, value, param_name='', check_types=None):
if not hasattr(value, 'resolve_expression'):
if check_types and not isinstance(value, check_types):
raise TypeError(
"The %s parameter has the wrong type: should be %s."
% (param_name, check_types)
"The %s parameter has the wrong type: should be %s." % (
param_name, check_types)
)
return value
@@ -120,17 +100,13 @@ class SQLiteDecimalToFloatMixin:
By default, Decimal values are converted to str by the SQLite backend, which
is not acceptable by the GIS functions expecting numeric values.
"""
def as_sqlite(self, compiler, connection, **extra_context):
copy = self.copy()
copy.set_source_expressions(
[
Value(float(expr.value))
if hasattr(expr, "value") and isinstance(expr.value, Decimal)
else expr
for expr in copy.get_source_expressions()
]
)
copy.set_source_expressions([
Value(float(expr.value)) if hasattr(expr, 'value') and isinstance(expr.value, Decimal)
else expr
for expr in copy.get_source_expressions()
])
return copy.as_sql(compiler, connection, **extra_context)
@@ -138,13 +114,11 @@ class OracleToleranceMixin:
tolerance = 0.05
def as_oracle(self, compiler, connection, **extra_context):
tolerance = Value(
self._handle_param(
self.extra.get("tolerance", self.tolerance),
"tolerance",
NUMERIC_TYPES,
)
)
tolerance = Value(self._handle_param(
self.extra.get('tolerance', self.tolerance),
'tolerance',
NUMERIC_TYPES,
))
clone = self.copy()
clone.set_source_expressions([*self.get_source_expressions(), tolerance])
return clone.as_sql(compiler, connection, **extra_context)
@@ -158,18 +132,14 @@ class Area(OracleToleranceMixin, GeoFunc):
return AreaField(self.geo_field)
def as_sql(self, compiler, connection, **extra_context):
if not connection.features.supports_area_geodetic and self.geo_field.geodetic(
connection
):
raise NotSupportedError(
"Area on geodetic coordinate systems not supported."
)
if not connection.features.supports_area_geodetic and self.geo_field.geodetic(connection):
raise NotSupportedError('Area on geodetic coordinate systems not supported.')
return super().as_sql(compiler, connection, **extra_context)
def as_sqlite(self, compiler, connection, **extra_context):
if self.geo_field.geodetic(connection):
extra_context["template"] = "%(function)s(%(expressions)s, %(spheroid)d)"
extra_context["spheroid"] = True
extra_context['template'] = '%(function)s(%(expressions)s, %(spheroid)d)'
extra_context['spheroid'] = True
return self.as_sql(compiler, connection, **extra_context)
@@ -185,7 +155,7 @@ class AsGeoJSON(GeoFunc):
def __init__(self, expression, bbox=False, crs=False, precision=8, **extra):
expressions = [expression]
if precision is not None:
expressions.append(self._handle_param(precision, "precision", int))
expressions.append(self._handle_param(precision, 'precision', int))
options = 0
if crs and bbox:
options = 3
@@ -211,7 +181,7 @@ class AsGML(GeoFunc):
def __init__(self, expression, version=2, precision=8, **extra):
expressions = [version, expression]
if precision is not None:
expressions.append(self._handle_param(precision, "precision", int))
expressions.append(self._handle_param(precision, 'precision', int))
super().__init__(*expressions, **extra)
def as_oracle(self, compiler, connection, **extra_context):
@@ -219,11 +189,7 @@ class AsGML(GeoFunc):
version = source_expressions[0]
clone = self.copy()
clone.set_source_expressions([source_expressions[1]])
extra_context["function"] = (
"SDO_UTIL.TO_GML311GEOMETRY"
if version.value == 3
else "SDO_UTIL.TO_GMLGEOMETRY"
)
extra_context['function'] = 'SDO_UTIL.TO_GML311GEOMETRY' if version.value == 3 else 'SDO_UTIL.TO_GMLGEOMETRY'
return super(AsGML, clone).as_sql(compiler, connection, **extra_context)
@@ -233,7 +199,7 @@ class AsKML(GeoFunc):
def __init__(self, expression, precision=8, **extra):
expressions = [expression]
if precision is not None:
expressions.append(self._handle_param(precision, "precision", int))
expressions.append(self._handle_param(precision, 'precision', int))
super().__init__(*expressions, **extra)
@@ -241,13 +207,11 @@ class AsSVG(GeoFunc):
output_field = TextField()
def __init__(self, expression, relative=False, precision=8, **extra):
relative = (
relative if hasattr(relative, "resolve_expression") else int(relative)
)
relative = relative if hasattr(relative, 'resolve_expression') else int(relative)
expressions = [
expression,
relative,
self._handle_param(precision, "precision", int),
self._handle_param(precision, 'precision', int),
]
super().__init__(*expressions, **extra)
@@ -269,9 +233,7 @@ class BoundingCircle(OracleToleranceMixin, GeomOutputGeoFunc):
def as_oracle(self, compiler, connection, **extra_context):
clone = self.copy()
clone.set_source_expressions([self.get_source_expressions()[0]])
return super(BoundingCircle, clone).as_oracle(
compiler, connection, **extra_context
)
return super(BoundingCircle, clone).as_oracle(compiler, connection, **extra_context)
class Centroid(OracleToleranceMixin, GeomOutputGeoFunc):
@@ -299,7 +261,7 @@ class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
def __init__(self, expr1, expr2, spheroid=None, **extra):
expressions = [expr1, expr2]
if spheroid is not None:
self.spheroid = self._handle_param(spheroid, "spheroid", bool)
self.spheroid = self._handle_param(spheroid, 'spheroid', bool)
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection, **extra_context):
@@ -317,29 +279,21 @@ class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
)
if not geography and self.geo_field.geodetic(connection):
# Geometry fields with geodetic (lon/lat) coordinates need special
# distance functions.
# Geometry fields with geodetic (lon/lat) coordinates need special distance functions
if self.spheroid:
# DistanceSpheroid is more accurate and resource intensive than
# DistanceSphere.
function = connection.ops.spatial_function_name("DistanceSpheroid")
# DistanceSpheroid is more accurate and resource intensive than DistanceSphere
function = connection.ops.spatial_function_name('DistanceSpheroid')
# Replace boolean param by the real spheroid of the base field
clone.source_expressions.append(
Value(self.geo_field.spheroid(connection))
)
clone.source_expressions.append(Value(self.geo_field.spheroid(connection)))
else:
function = connection.ops.spatial_function_name("DistanceSphere")
return super(Distance, clone).as_sql(
compiler, connection, function=function, **extra_context
)
function = connection.ops.spatial_function_name('DistanceSphere')
return super(Distance, clone).as_sql(compiler, connection, function=function, **extra_context)
def as_sqlite(self, compiler, connection, **extra_context):
if self.geo_field.geodetic(connection):
# SpatiaLite returns NULL instead of zero on geodetic coordinates
extra_context[
"template"
] = "COALESCE(%(function)s(%(expressions)s, %(spheroid)s), 0)"
extra_context["spheroid"] = int(bool(self.spheroid))
extra_context['template'] = 'COALESCE(%(function)s(%(expressions)s, %(spheroid)s), 0)'
extra_context['spheroid'] = int(bool(self.spheroid))
return super().as_sql(compiler, connection, **extra_context)
@@ -357,7 +311,7 @@ class GeoHash(GeoFunc):
def __init__(self, expression, precision=None, **extra):
expressions = [expression]
if precision is not None:
expressions.append(self._handle_param(precision, "precision", int))
expressions.append(self._handle_param(precision, 'precision', int))
super().__init__(*expressions, **extra)
def as_mysql(self, compiler, connection, **extra_context):
@@ -371,8 +325,8 @@ class GeoHash(GeoFunc):
class GeometryDistance(GeoFunc):
output_field = FloatField()
arity = 2
function = ""
arg_joiner = " <-> "
function = ''
arg_joiner = ' <-> '
geom_param_pos = (0, 1)
@@ -383,7 +337,7 @@ class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
@BaseSpatialField.register_lookup
class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform):
lookup_name = "isvalid"
lookup_name = 'isvalid'
output_field = BooleanField()
def as_oracle(self, compiler, connection, **extra_context):
@@ -397,13 +351,8 @@ class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
super().__init__(expr1, **extra)
def as_sql(self, compiler, connection, **extra_context):
if (
self.geo_field.geodetic(connection)
and not connection.features.supports_length_geodetic
):
raise NotSupportedError(
"This backend doesn't support Length on geodetic fields"
)
if self.geo_field.geodetic(connection) and not connection.features.supports_length_geodetic:
raise NotSupportedError("This backend doesn't support Length on geodetic fields")
return super().as_sql(compiler, connection, **extra_context)
def as_postgresql(self, compiler, connection, **extra_context):
@@ -413,20 +362,18 @@ class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
clone.source_expressions.append(Value(self.spheroid))
elif self.geo_field.geodetic(connection):
# Geometry fields with geodetic (lon/lat) coordinates need length_spheroid
function = connection.ops.spatial_function_name("LengthSpheroid")
function = connection.ops.spatial_function_name('LengthSpheroid')
clone.source_expressions.append(Value(self.geo_field.spheroid(connection)))
else:
dim = min(f.dim for f in self.get_source_fields() if f)
if dim > 2:
function = connection.ops.length3d
return super(Length, clone).as_sql(
compiler, connection, function=function, **extra_context
)
return super(Length, clone).as_sql(compiler, connection, function=function, **extra_context)
def as_sqlite(self, compiler, connection, **extra_context):
function = None
if self.geo_field.geodetic(connection):
function = "GeodesicLength" if self.spheroid else "GreatCircleLength"
function = 'GeodesicLength' if self.spheroid else 'GreatCircleLength'
return super().as_sql(compiler, connection, function=function, **extra_context)
@@ -461,9 +408,7 @@ class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
def as_postgresql(self, compiler, connection, **extra_context):
function = None
if self.geo_field.geodetic(connection) and not self.source_is_geography():
raise NotSupportedError(
"ST_Perimeter cannot use a non-projected non-geography field."
)
raise NotSupportedError("ST_Perimeter cannot use a non-projected non-geography field.")
dim = min(f.dim for f in self.get_source_fields())
if dim > 2:
function = connection.ops.perimeter3d
@@ -487,11 +432,11 @@ class Scale(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
def __init__(self, expression, x, y, z=0.0, **extra):
expressions = [
expression,
self._handle_param(x, "x", NUMERIC_TYPES),
self._handle_param(y, "y", NUMERIC_TYPES),
self._handle_param(x, 'x', NUMERIC_TYPES),
self._handle_param(y, 'y', NUMERIC_TYPES),
]
if z != 0.0:
expressions.append(self._handle_param(z, "z", NUMERIC_TYPES))
expressions.append(self._handle_param(z, 'z', NUMERIC_TYPES))
super().__init__(*expressions, **extra)
@@ -501,16 +446,16 @@ class SnapToGrid(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
expressions = [expression]
if nargs in (1, 2):
expressions.extend(
[self._handle_param(arg, "", NUMERIC_TYPES) for arg in args]
[self._handle_param(arg, '', NUMERIC_TYPES) for arg in args]
)
elif nargs == 4:
# Reverse origin and size param ordering
expressions += [
*(self._handle_param(arg, "", NUMERIC_TYPES) for arg in args[2:]),
*(self._handle_param(arg, "", NUMERIC_TYPES) for arg in args[0:2]),
*(self._handle_param(arg, '', NUMERIC_TYPES) for arg in args[2:]),
*(self._handle_param(arg, '', NUMERIC_TYPES) for arg in args[0:2]),
]
else:
raise ValueError("Must provide 1, 2, or 4 arguments to `SnapToGrid`.")
raise ValueError('Must provide 1, 2, or 4 arguments to `SnapToGrid`.')
super().__init__(*expressions, **extra)
@@ -523,10 +468,10 @@ class Transform(GeomOutputGeoFunc):
def __init__(self, expression, srid, **extra):
expressions = [
expression,
self._handle_param(srid, "srid", int),
self._handle_param(srid, 'srid', int),
]
if "output_field" not in extra:
extra["output_field"] = GeometryField(srid=srid)
if 'output_field' not in extra:
extra['output_field'] = GeometryField(srid=srid)
super().__init__(*expressions, **extra)
@@ -27,10 +27,10 @@ class GISLookup(Lookup):
def process_rhs_params(self):
if self.rhs_params:
# Check if a band index was passed in the query argument.
if len(self.rhs_params) == (2 if self.lookup_name == "relate" else 1):
if len(self.rhs_params) == (2 if self.lookup_name == 'relate' else 1):
self.process_band_indices()
elif len(self.rhs_params) > 1:
raise ValueError("Tuple too long for lookup %s." % self.lookup_name)
raise ValueError('Tuple too long for lookup %s.' % self.lookup_name)
elif isinstance(self.lhs, RasterBandTransform):
self.process_band_indices(only_lhs=True)
@@ -55,7 +55,7 @@ class GISLookup(Lookup):
def get_db_prep_lookup(self, value, connection):
# get_db_prep_lookup is called by process_rhs from super class
return ("%s", [connection.ops.Adapter(value)])
return ('%s', [connection.ops.Adapter(value)])
def process_rhs(self, compiler, connection):
if isinstance(self.rhs, Query):
@@ -64,9 +64,7 @@ class GISLookup(Lookup):
if isinstance(self.rhs, Expression):
self.rhs = self.rhs.resolve_expression(compiler.query)
rhs, rhs_params = super().process_rhs(compiler, connection)
placeholder = connection.ops.get_geom_placeholder(
self.lhs.output_field, self.rhs, compiler
)
placeholder = connection.ops.get_geom_placeholder(self.lhs.output_field, self.rhs, compiler)
return placeholder % rhs, rhs_params
def get_rhs_op(self, connection, rhs):
@@ -80,12 +78,7 @@ class GISLookup(Lookup):
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
sql_params = (*lhs_params, *rhs_params)
template_params = {
"lhs": lhs_sql,
"rhs": rhs_sql,
"value": "%s",
**self.template_params,
}
template_params = {'lhs': lhs_sql, 'rhs': rhs_sql, 'value': '%s', **self.template_params}
rhs_op = self.get_rhs_op(connection, rhs_sql)
return rhs_op.as_sql(connection, self, template_params, sql_params)
@@ -94,15 +87,13 @@ class GISLookup(Lookup):
# Geometry operators
# ------------------
@BaseSpatialField.register_lookup
class OverlapsLeftLookup(GISLookup):
"""
The overlaps_left operator returns true if A's bounding box overlaps or is to the
left of B's bounding box.
"""
lookup_name = "overlaps_left"
lookup_name = 'overlaps_left'
@BaseSpatialField.register_lookup
@@ -111,8 +102,7 @@ class OverlapsRightLookup(GISLookup):
The 'overlaps_right' operator returns true if A's bounding box overlaps or is to the
right of B's bounding box.
"""
lookup_name = "overlaps_right"
lookup_name = 'overlaps_right'
@BaseSpatialField.register_lookup
@@ -121,8 +111,7 @@ class OverlapsBelowLookup(GISLookup):
The 'overlaps_below' operator returns true if A's bounding box overlaps or is below
B's bounding box.
"""
lookup_name = "overlaps_below"
lookup_name = 'overlaps_below'
@BaseSpatialField.register_lookup
@@ -131,8 +120,7 @@ class OverlapsAboveLookup(GISLookup):
The 'overlaps_above' operator returns true if A's bounding box overlaps or is above
B's bounding box.
"""
lookup_name = "overlaps_above"
lookup_name = 'overlaps_above'
@BaseSpatialField.register_lookup
@@ -141,8 +129,7 @@ class LeftLookup(GISLookup):
The 'left' operator returns true if A's bounding box is strictly to the left
of B's bounding box.
"""
lookup_name = "left"
lookup_name = 'left'
@BaseSpatialField.register_lookup
@@ -151,8 +138,7 @@ class RightLookup(GISLookup):
The 'right' operator returns true if A's bounding box is strictly to the right
of B's bounding box.
"""
lookup_name = "right"
lookup_name = 'right'
@BaseSpatialField.register_lookup
@@ -161,8 +147,7 @@ class StrictlyBelowLookup(GISLookup):
The 'strictly_below' operator returns true if A's bounding box is strictly below B's
bounding box.
"""
lookup_name = "strictly_below"
lookup_name = 'strictly_below'
@BaseSpatialField.register_lookup
@@ -171,8 +156,7 @@ class StrictlyAboveLookup(GISLookup):
The 'strictly_above' operator returns true if A's bounding box is strictly above B's
bounding box.
"""
lookup_name = "strictly_above"
lookup_name = 'strictly_above'
@BaseSpatialField.register_lookup
@@ -182,11 +166,10 @@ class SameAsLookup(GISLookup):
equality of two features. So if A and B are the same feature,
vertex-by-vertex, the operator returns true.
"""
lookup_name = "same_as"
lookup_name = 'same_as'
BaseSpatialField.register_lookup(SameAsLookup, "exact")
BaseSpatialField.register_lookup(SameAsLookup, 'exact')
@BaseSpatialField.register_lookup
@@ -195,18 +178,15 @@ class BBContainsLookup(GISLookup):
The 'bbcontains' operator returns true if A's bounding box completely contains
by B's bounding box.
"""
lookup_name = "bbcontains"
lookup_name = 'bbcontains'
@BaseSpatialField.register_lookup
class BBOverlapsLookup(GISLookup):
"""
The 'bboverlaps' operator returns true if A's bounding box overlaps B's
bounding box.
The 'bboverlaps' operator returns true if A's bounding box overlaps B's bounding box.
"""
lookup_name = "bboverlaps"
lookup_name = 'bboverlaps'
@BaseSpatialField.register_lookup
@@ -215,71 +195,69 @@ class ContainedLookup(GISLookup):
The 'contained' operator returns true if A's bounding box is completely contained
by B's bounding box.
"""
lookup_name = "contained"
lookup_name = 'contained'
# ------------------
# Geometry functions
# ------------------
@BaseSpatialField.register_lookup
class ContainsLookup(GISLookup):
lookup_name = "contains"
lookup_name = 'contains'
@BaseSpatialField.register_lookup
class ContainsProperlyLookup(GISLookup):
lookup_name = "contains_properly"
lookup_name = 'contains_properly'
@BaseSpatialField.register_lookup
class CoveredByLookup(GISLookup):
lookup_name = "coveredby"
lookup_name = 'coveredby'
@BaseSpatialField.register_lookup
class CoversLookup(GISLookup):
lookup_name = "covers"
lookup_name = 'covers'
@BaseSpatialField.register_lookup
class CrossesLookup(GISLookup):
lookup_name = "crosses"
lookup_name = 'crosses'
@BaseSpatialField.register_lookup
class DisjointLookup(GISLookup):
lookup_name = "disjoint"
lookup_name = 'disjoint'
@BaseSpatialField.register_lookup
class EqualsLookup(GISLookup):
lookup_name = "equals"
lookup_name = 'equals'
@BaseSpatialField.register_lookup
class IntersectsLookup(GISLookup):
lookup_name = "intersects"
lookup_name = 'intersects'
@BaseSpatialField.register_lookup
class OverlapsLookup(GISLookup):
lookup_name = "overlaps"
lookup_name = 'overlaps'
@BaseSpatialField.register_lookup
class RelateLookup(GISLookup):
lookup_name = "relate"
sql_template = "%(func)s(%(lhs)s, %(rhs)s, %%s)"
pattern_regex = _lazy_re_compile(r"^[012TF\*]{9}$")
lookup_name = 'relate'
sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)'
pattern_regex = _lazy_re_compile(r'^[012TF\*]{9}$')
def process_rhs(self, compiler, connection):
# Check the pattern argument
pattern = self.rhs_params[0]
backend_op = connection.ops.gis_operators[self.lookup_name]
if hasattr(backend_op, "check_relate_argument"):
if hasattr(backend_op, 'check_relate_argument'):
backend_op.check_relate_argument(pattern)
elif not isinstance(pattern, str) or not self.pattern_regex.match(pattern):
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
@@ -289,107 +267,93 @@ class RelateLookup(GISLookup):
@BaseSpatialField.register_lookup
class TouchesLookup(GISLookup):
lookup_name = "touches"
lookup_name = 'touches'
@BaseSpatialField.register_lookup
class WithinLookup(GISLookup):
lookup_name = "within"
lookup_name = 'within'
class DistanceLookupBase(GISLookup):
distance = True
sql_template = "%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s"
sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s'
def process_rhs_params(self):
if not 1 <= len(self.rhs_params) <= 3:
raise ValueError(
"2, 3, or 4-element tuple required for '%s' lookup." % self.lookup_name
)
elif len(self.rhs_params) == 3 and self.rhs_params[2] != "spheroid":
raise ValueError(
"For 4-element tuples the last argument must be the 'spheroid' "
"directive."
)
raise ValueError("2, 3, or 4-element tuple required for '%s' lookup." % self.lookup_name)
elif len(self.rhs_params) == 3 and self.rhs_params[2] != 'spheroid':
raise ValueError("For 4-element tuples the last argument must be the 'spheroid' directive.")
# Check if the second parameter is a band index.
if len(self.rhs_params) > 1 and self.rhs_params[1] != "spheroid":
if len(self.rhs_params) > 1 and self.rhs_params[1] != 'spheroid':
self.process_band_indices()
def process_distance(self, compiler, connection):
dist_param = self.rhs_params[0]
return (
compiler.compile(dist_param.resolve_expression(compiler.query))
if hasattr(dist_param, "resolve_expression")
else (
"%s",
connection.ops.get_distance(
self.lhs.output_field, self.rhs_params, self.lookup_name
),
)
if hasattr(dist_param, 'resolve_expression') else
('%s', connection.ops.get_distance(self.lhs.output_field, self.rhs_params, self.lookup_name))
)
@BaseSpatialField.register_lookup
class DWithinLookup(DistanceLookupBase):
lookup_name = "dwithin"
sql_template = "%(func)s(%(lhs)s, %(rhs)s, %(value)s)"
lookup_name = 'dwithin'
sql_template = '%(func)s(%(lhs)s, %(rhs)s, %(value)s)'
def process_distance(self, compiler, connection):
dist_param = self.rhs_params[0]
if (
not connection.features.supports_dwithin_distance_expr
and hasattr(dist_param, "resolve_expression")
and not isinstance(dist_param, Distance)
not connection.features.supports_dwithin_distance_expr and
hasattr(dist_param, 'resolve_expression') and
not isinstance(dist_param, Distance)
):
raise NotSupportedError(
"This backend does not support expressions for specifying "
"distance in the dwithin lookup."
'This backend does not support expressions for specifying '
'distance in the dwithin lookup.'
)
return super().process_distance(compiler, connection)
def process_rhs(self, compiler, connection):
dist_sql, dist_params = self.process_distance(compiler, connection)
self.template_params["value"] = dist_sql
self.template_params['value'] = dist_sql
rhs_sql, params = super().process_rhs(compiler, connection)
return rhs_sql, params + dist_params
class DistanceLookupFromFunction(DistanceLookupBase):
def as_sql(self, compiler, connection):
spheroid = (
len(self.rhs_params) == 2 and self.rhs_params[-1] == "spheroid"
) or None
distance_expr = connection.ops.distance_expr_for_lookup(
self.lhs, self.rhs, spheroid=spheroid
)
spheroid = (len(self.rhs_params) == 2 and self.rhs_params[-1] == 'spheroid') or None
distance_expr = connection.ops.distance_expr_for_lookup(self.lhs, self.rhs, spheroid=spheroid)
sql, params = compiler.compile(distance_expr.resolve_expression(compiler.query))
dist_sql, dist_params = self.process_distance(compiler, connection)
return (
"%(func)s %(op)s %(dist)s" % {"func": sql, "op": self.op, "dist": dist_sql},
'%(func)s %(op)s %(dist)s' % {'func': sql, 'op': self.op, 'dist': dist_sql},
params + dist_params,
)
@BaseSpatialField.register_lookup
class DistanceGTLookup(DistanceLookupFromFunction):
lookup_name = "distance_gt"
op = ">"
lookup_name = 'distance_gt'
op = '>'
@BaseSpatialField.register_lookup
class DistanceGTELookup(DistanceLookupFromFunction):
lookup_name = "distance_gte"
op = ">="
lookup_name = 'distance_gte'
op = '>='
@BaseSpatialField.register_lookup
class DistanceLTLookup(DistanceLookupFromFunction):
lookup_name = "distance_lt"
op = "<"
lookup_name = 'distance_lt'
op = '<'
@BaseSpatialField.register_lookup
class DistanceLTELookup(DistanceLookupFromFunction):
lookup_name = "distance_lte"
op = "<="
lookup_name = 'distance_lte'
op = '<='
@@ -37,7 +37,7 @@ class SpatialProxy(DeferredAttribute):
if isinstance(geo_value, self._klass):
geo_obj = geo_value
elif (geo_value is None) or (geo_value == ""):
elif (geo_value is None) or (geo_value == ''):
geo_obj = None
else:
# Otherwise, a geometry or raster object is built using the field's
@@ -57,10 +57,8 @@ class SpatialProxy(DeferredAttribute):
# The geographic type of the field.
gtype = self.field.geom_type
if gtype == "RASTER" and (
value is None or isinstance(value, (str, dict, self._klass))
):
# For raster fields, ensure input is None or a string, dict, or
if gtype == 'RASTER' and (value is None or isinstance(value, (str, dict, self._klass))):
# For raster fields, assure input is None or a string, dict, or
# raster instance.
pass
elif isinstance(value, self._klass):
@@ -73,10 +71,8 @@ class SpatialProxy(DeferredAttribute):
# Set geometries with None, WKT, HEX, or WKB
pass
else:
raise TypeError(
"Cannot set %s SpatialProxy (%s) with value of type: %s"
% (instance.__class__.__name__, gtype, type(value))
)
raise TypeError('Cannot set %s SpatialProxy (%s) with value of type: %s' % (
instance.__class__.__name__, gtype, type(value)))
# Setting the objects dictionary with the value, and returning.
instance.__dict__[self.field.attname] = value
@@ -1,6 +1,7 @@
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField
from django.contrib.gis.db.models.sql.conversion import (
AreaField, DistanceField,
)
__all__ = [
"AreaField",
"DistanceField",
'AreaField', 'DistanceField',
]
@@ -10,14 +10,13 @@ from django.db import models
class AreaField(models.FloatField):
"Wrapper for Area values."
def __init__(self, geo_field):
super().__init__()
self.geo_field = geo_field
def get_prep_value(self, value):
if not isinstance(value, Area):
raise ValueError("AreaField only accepts Area measurement objects.")
raise ValueError('AreaField only accepts Area measurement objects.')
return value
def get_db_prep_value(self, value, connection, prepared=False):
@@ -38,12 +37,11 @@ class AreaField(models.FloatField):
return Area(**{area_att: value}) if area_att else value
def get_internal_type(self):
return "AreaField"
return 'AreaField'
class DistanceField(models.FloatField):
"Wrapper for Distance values."
def __init__(self, geo_field):
super().__init__()
self.geo_field = geo_field
@@ -58,9 +56,7 @@ class DistanceField(models.FloatField):
return value
distance_att = connection.ops.get_distance_att_for_field(self.geo_field)
if not distance_att:
raise ValueError(
"Distance measure is supplied, but units are unknown for result."
)
raise ValueError('Distance measure is supplied, but units are unknown for result.')
return getattr(value, distance_att)
def from_db_value(self, value, expression, connection):
@@ -70,4 +66,4 @@ class DistanceField(models.FloatField):
return Distance(**{distance_att: value}) if distance_att else value
def get_internal_type(self):
return "DistanceField"
return 'DistanceField'