init commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
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
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.wkt, self.srid))
|
||||
|
||||
def __str__(self):
|
||||
return self.wkt
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly):
|
||||
# Hook for Oracle.
|
||||
return poly
|
||||
@@ -0,0 +1,111 @@
|
||||
import re
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
|
||||
|
||||
class BaseSpatialFeatures:
|
||||
gis_enabled = True
|
||||
|
||||
# Does the database contain a SpatialRefSys model to store SRID information?
|
||||
has_spatialrefsys_table = True
|
||||
|
||||
# Does the backend support the django.contrib.gis.utils.add_srs_entry() utility?
|
||||
supports_add_srs_entry = True
|
||||
# Does the backend introspect GeometryField to its subtypes?
|
||||
supports_geometry_field_introspection = True
|
||||
|
||||
# Does the database have a geography type?
|
||||
supports_geography = False
|
||||
# Does the backend support storing 3D geometries?
|
||||
supports_3d_storage = False
|
||||
# Reference implementation of 3D functions is:
|
||||
# https://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
|
||||
supports_3d_functions = False
|
||||
# Does the database support SRID transform operations?
|
||||
supports_transform = True
|
||||
# Can geometry fields be null?
|
||||
supports_null_geometries = True
|
||||
# Are empty geometries supported?
|
||||
supports_empty_geometries = False
|
||||
# Can the function be applied on geodetic coordinate systems?
|
||||
supports_distance_geodetic = True
|
||||
supports_length_geodetic = True
|
||||
supports_perimeter_geodetic = False
|
||||
supports_area_geodetic = True
|
||||
# Is the database able to count vertices on polygons (with `num_points`)?
|
||||
supports_num_points_poly = True
|
||||
|
||||
# Does the backend support expressions for specifying distance in the
|
||||
# dwithin lookup?
|
||||
supports_dwithin_distance_expr = True
|
||||
|
||||
# Does the database have raster support?
|
||||
supports_raster = False
|
||||
|
||||
# Does the database support a unique index on geometry fields?
|
||||
supports_geometry_field_unique_index = True
|
||||
|
||||
# Can SchemaEditor alter geometry fields?
|
||||
can_alter_geometry_field = True
|
||||
|
||||
# Do the database functions/aggregates support the tolerance parameter?
|
||||
supports_tolerance_parameter = False
|
||||
|
||||
# Set of options that AsGeoJSON() doesn't support.
|
||||
unsupported_geojson_options = {}
|
||||
|
||||
# Does Intersection() return None (rather than an empty GeometryCollection)
|
||||
# for empty results?
|
||||
empty_intersection_returns_none = True
|
||||
|
||||
@property
|
||||
def supports_bbcontains_lookup(self):
|
||||
return "bbcontains" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_contained_lookup(self):
|
||||
return "contained" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_crosses_lookup(self):
|
||||
return "crosses" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_distances_lookups(self):
|
||||
return self.has_Distance_function
|
||||
|
||||
@property
|
||||
def supports_dwithin_lookup(self):
|
||||
return "dwithin" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_relate_lookup(self):
|
||||
return "relate" in self.connection.ops.gis_operators
|
||||
|
||||
@property
|
||||
def supports_isvalid_lookup(self):
|
||||
return self.has_IsValid_function
|
||||
|
||||
# Is the aggregate supported by the database?
|
||||
@property
|
||||
def supports_collect_aggr(self):
|
||||
return models.Collect not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_extent_aggr(self):
|
||||
return models.Extent not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_make_line_aggr(self):
|
||||
return models.MakeLine not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
@property
|
||||
def supports_union_aggr(self):
|
||||
return models.Union not in self.connection.ops.disallowed_aggregates
|
||||
|
||||
def __getattr__(self, 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
|
||||
raise AttributeError
|
||||
@@ -0,0 +1,140 @@
|
||||
from django.contrib.gis import gdal
|
||||
|
||||
|
||||
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"):
|
||||
# Returning a clone of the cached SpatialReference object.
|
||||
return self._srs.clone()
|
||||
else:
|
||||
# Attempting to cache a SpatialReference object.
|
||||
|
||||
# Trying to get from WKT first.
|
||||
try:
|
||||
self._srs = gdal.SpatialReference(self.wkt)
|
||||
return self.srs
|
||||
except Exception as e:
|
||||
msg = e
|
||||
|
||||
try:
|
||||
self._srs = gdal.SpatialReference(self.proj4text)
|
||||
return self.srs
|
||||
except Exception as e:
|
||||
msg = e
|
||||
|
||||
raise Exception(
|
||||
"Could not get OSR SpatialReference from WKT: %s\nError:\n%s"
|
||||
% (self.wkt, msg)
|
||||
)
|
||||
|
||||
@property
|
||||
def ellipsoid(self):
|
||||
"""
|
||||
Return a tuple of the ellipsoid parameters:
|
||||
(semimajor axis, semiminor axis, and inverse flattening).
|
||||
"""
|
||||
return self.srs.ellipsoid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Return the projection name."
|
||||
return self.srs.name
|
||||
|
||||
@property
|
||||
def spheroid(self):
|
||||
"Return the spheroid name for this spatial reference."
|
||||
return self.srs["spheroid"]
|
||||
|
||||
@property
|
||||
def datum(self):
|
||||
"Return the datum for this spatial reference."
|
||||
return self.srs["datum"]
|
||||
|
||||
@property
|
||||
def projected(self):
|
||||
"Is this Spatial Reference projected?"
|
||||
return self.srs.projected
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
"Is this Spatial Reference local?"
|
||||
return self.srs.local
|
||||
|
||||
@property
|
||||
def geographic(self):
|
||||
"Is this Spatial Reference geographic?"
|
||||
return self.srs.geographic
|
||||
|
||||
@property
|
||||
def linear_name(self):
|
||||
"Return the linear units name."
|
||||
return self.srs.linear_name
|
||||
|
||||
@property
|
||||
def linear_units(self):
|
||||
"Return the linear units."
|
||||
return self.srs.linear_units
|
||||
|
||||
@property
|
||||
def angular_name(self):
|
||||
"Return the name of the angular units."
|
||||
return self.srs.angular_name
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Return the angular units."
|
||||
return self.srs.angular_units
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"Return a tuple of the units and the name."
|
||||
if self.projected or self.local:
|
||||
return (self.linear_units, self.linear_name)
|
||||
elif self.geographic:
|
||||
return (self.angular_units, self.angular_name)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
@classmethod
|
||||
def get_units(cls, wkt):
|
||||
"""
|
||||
Return a tuple of (unit_value, unit_name) for the given WKT without
|
||||
using any of the database fields.
|
||||
"""
|
||||
return gdal.SpatialReference(wkt).units
|
||||
|
||||
@classmethod
|
||||
def get_spheroid(cls, wkt, string=True):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||
"""
|
||||
srs = gdal.SpatialReference(wkt)
|
||||
sphere_params = srs.ellipsoid
|
||||
sphere_name = srs["spheroid"]
|
||||
|
||||
if not string:
|
||||
return sphere_name, sphere_params
|
||||
else:
|
||||
# `string` parameter used to place in format acceptable by PostGIS
|
||||
if len(sphere_params) == 3:
|
||||
radius, flattening = sphere_params[0], sphere_params[2]
|
||||
else:
|
||||
radius, flattening = sphere_params
|
||||
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return the string representation, a 'pretty' OGC WKT.
|
||||
"""
|
||||
return str(self.srs)
|
||||
@@ -0,0 +1,210 @@
|
||||
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.db import NotSupportedError
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class BaseSpatialOperations:
|
||||
# Quick booleans for the type of this spatial backend, and
|
||||
# an attribute for the spatial database version tuple (if applicable)
|
||||
postgis = False
|
||||
spatialite = False
|
||||
mariadb = False
|
||||
mysql = False
|
||||
oracle = False
|
||||
spatial_version = None
|
||||
|
||||
# How the geometry column should be selected.
|
||||
select = "%s"
|
||||
|
||||
@cached_property
|
||||
def select_extent(self):
|
||||
return self.select
|
||||
|
||||
# Aggregates
|
||||
disallowed_aggregates = ()
|
||||
|
||||
geom_func_prefix = ""
|
||||
|
||||
# Mapping between Django function names and backend names, when names do not
|
||||
# match; used in spatial_function_name().
|
||||
function_names = {}
|
||||
|
||||
# Set of known unsupported functions of the backend
|
||||
unsupported_functions = {
|
||||
"Area",
|
||||
"AsGeoJSON",
|
||||
"AsGML",
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"BoundingCircle",
|
||||
"Centroid",
|
||||
"ClosestPoint",
|
||||
"Difference",
|
||||
"Distance",
|
||||
"Envelope",
|
||||
"FromWKB",
|
||||
"FromWKT",
|
||||
"GeoHash",
|
||||
"GeometryDistance",
|
||||
"Intersection",
|
||||
"IsEmpty",
|
||||
"IsValid",
|
||||
"Length",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"NumGeometries",
|
||||
"NumPoints",
|
||||
"Perimeter",
|
||||
"PointOnSurface",
|
||||
"Reverse",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"SymDifference",
|
||||
"Transform",
|
||||
"Translate",
|
||||
"Union",
|
||||
}
|
||||
|
||||
# Constructors
|
||||
from_text = False
|
||||
|
||||
# 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."
|
||||
)
|
||||
|
||||
def convert_extent3d(self, box, srid):
|
||||
raise NotImplementedError(
|
||||
"Aggregate 3D extent not implemented for this spatial backend."
|
||||
)
|
||||
|
||||
# For quoting column values, rather than columns.
|
||||
def geo_quote_name(self, name):
|
||||
return "'%s'" % name
|
||||
|
||||
# GeometryField operations
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
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"
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
"""
|
||||
Return the placeholder for the given geometry field with the given
|
||||
value. Depending on the spatial backend, the placeholder may contain a
|
||||
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"):
|
||||
return (
|
||||
"%s(%%s, %s)" % (self.spatial_function_name("Transform"), f.srid)
|
||||
if transform_value(value.output_field, f)
|
||||
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,
|
||||
)
|
||||
elif self.connection.features.has_spatialrefsys_table:
|
||||
return "%s(%%s,%s)" % (self.from_text, f.srid)
|
||||
else:
|
||||
# For backwards compatibility on MySQL (#27464).
|
||||
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
|
||||
)
|
||||
super().check_expression_support(expression)
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
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
|
||||
)
|
||||
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."
|
||||
)
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseSpatialOperations must a provide spatial_ref_sys() "
|
||||
"method"
|
||||
)
|
||||
|
||||
distance_expr_for_lookup = staticmethod(Distance)
|
||||
|
||||
def get_db_converters(self, expression):
|
||||
converters = super().get_db_converters(expression)
|
||||
if isinstance(expression.output_field, GeometryField):
|
||||
converters.append(self.get_geometry_converter(expression))
|
||||
return converters
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
raise NotImplementedError(
|
||||
"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."
|
||||
)
|
||||
else:
|
||||
units_name = field.units_name(self.connection)
|
||||
if units_name:
|
||||
return AreaMeasure.unit_attname(units_name)
|
||||
|
||||
def get_distance_att_for_field(self, field):
|
||||
dist_att = None
|
||||
if field.geodetic(self.connection):
|
||||
if self.connection.features.supports_distance_geodetic:
|
||||
dist_att = "m"
|
||||
else:
|
||||
units = field.units_name(self.connection)
|
||||
if units:
|
||||
dist_att = DistanceMeasure.unit_attname(units)
|
||||
return dist_att
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
|
||||
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import MySQLIntrospection
|
||||
from .operations import MySQLOperations
|
||||
from .schema import MySQLGISSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(MySQLDatabaseWrapper):
|
||||
SchemaEditorClass = MySQLGISSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = MySQLIntrospection
|
||||
ops_class = MySQLOperations
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
|
||||
empty_intersection_returns_none = False
|
||||
has_spatialrefsys_table = False
|
||||
supports_add_srs_entry = False
|
||||
supports_distance_geodetic = False
|
||||
supports_length_geodetic = False
|
||||
supports_area_geodetic = False
|
||||
supports_transform = False
|
||||
supports_null_geometries = False
|
||||
supports_num_points_poly = False
|
||||
unsupported_geojson_options = {"crs"}
|
||||
|
||||
@cached_property
|
||||
def supports_geometry_field_unique_index(self):
|
||||
# Not supported in MySQL since https://dev.mysql.com/worklog/task/?id=11808
|
||||
return self.connection.mysql_is_mariadb
|
||||
@@ -0,0 +1,33 @@
|
||||
from MySQLdb.constants import FIELD_TYPE
|
||||
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
||||
|
||||
|
||||
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"
|
||||
|
||||
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))
|
||||
# Increment over description info until we get to the geometry
|
||||
# column.
|
||||
for column, typ, null, key, default, extra in cursor.fetchall():
|
||||
if column == description.name:
|
||||
# Using OGRGeomType to convert from OGC name to Django field.
|
||||
# MySQL does not support 3D or SRIDs, so the field params
|
||||
# are empty.
|
||||
field_type = OGRGeomType(typ).django
|
||||
field_params = {}
|
||||
break
|
||||
return field_type, field_params
|
||||
|
||||
def supports_spatial_index(self, cursor, table_name):
|
||||
# Supported with MyISAM, Aria, or InnoDB.
|
||||
storage_engine = self.get_storage_engine(cursor, table_name)
|
||||
return storage_engine in ("MyISAM", "Aria", "InnoDB")
|
||||
@@ -0,0 +1,146 @@
|
||||
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.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db.backends.mysql.operations import DatabaseOperations
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "mysql"
|
||||
geom_func_prefix = "ST_"
|
||||
|
||||
Adapter = WKTAdapter
|
||||
|
||||
@cached_property
|
||||
def mariadb(self):
|
||||
return self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def mysql(self):
|
||||
return not self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def select(self):
|
||||
return self.geom_func_prefix + "AsBinary(%s)"
|
||||
|
||||
@cached_property
|
||||
def from_text(self):
|
||||
return self.geom_func_prefix + "GeomFromText"
|
||||
|
||||
@cached_property
|
||||
def collect(self):
|
||||
if self.connection.features.supports_collect_aggr:
|
||||
return self.geom_func_prefix + "Collect"
|
||||
|
||||
@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"),
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
operators["relate"] = SpatialOperator(func="ST_Relate")
|
||||
else:
|
||||
operators["covers"] = SpatialOperator(func="MBRCovers")
|
||||
operators["coveredby"] = SpatialOperator(func="MBRCoveredBy")
|
||||
return operators
|
||||
|
||||
@cached_property
|
||||
def disallowed_aggregates(self):
|
||||
disallowed_aggregates = [
|
||||
models.Extent,
|
||||
models.Extent3D,
|
||||
models.MakeLine,
|
||||
models.Union,
|
||||
]
|
||||
is_mariadb = self.connection.mysql_is_mariadb
|
||||
if is_mariadb or self.connection.mysql_version < (8, 0, 24):
|
||||
disallowed_aggregates.insert(0, models.Collect)
|
||||
return tuple(disallowed_aggregates)
|
||||
|
||||
function_names = {
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def unsupported_functions(self):
|
||||
unsupported = {
|
||||
"AsGML",
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"BoundingCircle",
|
||||
"ClosestPoint",
|
||||
"ForcePolygonCW",
|
||||
"GeometryDistance",
|
||||
"IsEmpty",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"Perimeter",
|
||||
"PointOnSurface",
|
||||
"Reverse",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"Transform",
|
||||
"Translate",
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
unsupported.remove("PointOnSurface")
|
||||
unsupported.update({"GeoHash", "IsValid"})
|
||||
return unsupported
|
||||
|
||||
def geo_db_type(self, f):
|
||||
return f.geom_type
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are allowed on "
|
||||
"geodetic distance queries."
|
||||
)
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
srid = expression.output_field.srid
|
||||
if srid == -1:
|
||||
srid = None
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if value is not None:
|
||||
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
|
||||
if srid:
|
||||
geom.srid = srid
|
||||
return geom
|
||||
|
||||
return converter
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
return getattr(self, agg_name.lower())
|
||||
@@ -0,0 +1,112 @@
|
||||
import logging
|
||||
|
||||
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")
|
||||
|
||||
|
||||
class MySQLGISSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)"
|
||||
|
||||
def skip_default(self, field):
|
||||
# Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13
|
||||
# doesn't support defaults.
|
||||
if (
|
||||
isinstance(field, GeometryField)
|
||||
and not self._supports_limited_data_type_defaults
|
||||
):
|
||||
return True
|
||||
return super().skip_default(field)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
return super().quote_value(value)
|
||||
|
||||
def _field_indexes_sql(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
|
||||
with self.connection.cursor() as cursor:
|
||||
supports_spatial_index = (
|
||||
self.connection.introspection.supports_spatial_index(
|
||||
cursor, model._meta.db_table
|
||||
)
|
||||
)
|
||||
sql = self._create_spatial_index_sql(model, field)
|
||||
if supports_spatial_index:
|
||||
return [sql]
|
||||
else:
|
||||
logger.error(
|
||||
f"Cannot create SPATIAL INDEX {sql}. Only MyISAM, Aria, and InnoDB "
|
||||
f"support them.",
|
||||
)
|
||||
return []
|
||||
return super()._field_indexes_sql(model, field)
|
||||
|
||||
def remove_field(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
|
||||
sql = self._delete_spatial_index_sql(model, field)
|
||||
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,
|
||||
)
|
||||
|
||||
super().remove_field(model, field)
|
||||
|
||||
def _alter_field(
|
||||
self,
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=False,
|
||||
):
|
||||
super()._alter_field(
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
old_field_spatial_index = (
|
||||
isinstance(old_field, GeometryField)
|
||||
and old_field.spatial_index
|
||||
and not old_field.null
|
||||
)
|
||||
new_field_spatial_index = (
|
||||
isinstance(new_field, GeometryField)
|
||||
and new_field.spatial_index
|
||||
and not new_field.null
|
||||
)
|
||||
if not old_field_spatial_index and new_field_spatial_index:
|
||||
self.execute(self._create_spatial_index_sql(model, new_field))
|
||||
elif old_field_spatial_index and not new_field_spatial_index:
|
||||
self.execute(self._delete_spatial_index_sql(model, old_field))
|
||||
|
||||
def _create_spatial_index_name(self, model, field):
|
||||
return "%s_%s_id" % (model._meta.db_table, field.column)
|
||||
|
||||
def _create_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
qn = self.connection.ops.quote_name
|
||||
return self.sql_add_spatial_index % {
|
||||
"index": qn(index_name),
|
||||
"table": qn(model._meta.db_table),
|
||||
"column": qn(field.column),
|
||||
}
|
||||
|
||||
def _delete_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
return self._delete_index_sql(model, index_name)
|
||||
@@ -0,0 +1,61 @@
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
from django.contrib.gis.geos import GeometryCollection, Polygon
|
||||
from django.db.backends.oracle.oracledb_any import oracledb
|
||||
|
||||
|
||||
class OracleSpatialAdapter(WKTAdapter):
|
||||
input_size = oracledb.CLOB
|
||||
|
||||
def __init__(self, geom):
|
||||
"""
|
||||
Oracle requires that polygon rings are in proper orientation. This
|
||||
affects spatial operations and an invalid orientation may cause
|
||||
failures. Correct orientations are:
|
||||
* Outer ring - counter clockwise
|
||||
* Inner ring(s) - clockwise
|
||||
"""
|
||||
if isinstance(geom, Polygon):
|
||||
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
|
||||
):
|
||||
geom = self._fix_geometry_collection(geom)
|
||||
|
||||
self.wkt = geom.wkt
|
||||
self.srid = geom.srid
|
||||
|
||||
@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)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly, clone=True):
|
||||
"""Fix single polygon orientation as described in __init__()."""
|
||||
if clone:
|
||||
poly = poly.clone()
|
||||
|
||||
if not poly.exterior_ring.is_counterclockwise:
|
||||
poly.exterior_ring = list(reversed(poly.exterior_ring))
|
||||
|
||||
for i in range(1, len(poly)):
|
||||
if poly[i].is_counterclockwise:
|
||||
poly[i] = list(reversed(poly[i]))
|
||||
|
||||
return poly
|
||||
|
||||
@classmethod
|
||||
def _fix_geometry_collection(cls, coll):
|
||||
"""
|
||||
Fix polygon orientations in geometry collections as described in
|
||||
__init__().
|
||||
"""
|
||||
coll = coll.clone()
|
||||
for i, geom in enumerate(coll):
|
||||
if isinstance(geom, Polygon):
|
||||
coll[i] = cls._fix_polygon(geom, clone=False)
|
||||
return coll
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper
|
||||
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import OracleIntrospection
|
||||
from .operations import OracleOperations
|
||||
from .schema import OracleGISSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(OracleDatabaseWrapper):
|
||||
SchemaEditorClass = OracleGISSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = OracleIntrospection
|
||||
ops_class = OracleOperations
|
||||
@@ -0,0 +1,28 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.oracle.features import (
|
||||
DatabaseFeatures as OracleDatabaseFeatures,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures):
|
||||
supports_add_srs_entry = False
|
||||
supports_geometry_field_introspection = False
|
||||
supports_geometry_field_unique_index = False
|
||||
supports_perimeter_geodetic = True
|
||||
supports_dwithin_distance_expr = False
|
||||
supports_tolerance_parameter = True
|
||||
unsupported_geojson_options = {"bbox", "crs", "precision"}
|
||||
|
||||
@cached_property
|
||||
def django_test_skips(self):
|
||||
skips = super().django_test_skips
|
||||
skips.update(
|
||||
{
|
||||
"Oracle doesn't support spatial operators in constraints.": {
|
||||
"gis_tests.gis_migrations.test_operations.OperationTests."
|
||||
"test_add_check_constraint",
|
||||
},
|
||||
}
|
||||
)
|
||||
return skips
|
||||
@@ -0,0 +1,46 @@
|
||||
from django.db.backends.oracle.introspection import DatabaseIntrospection
|
||||
from django.db.backends.oracle.oracledb_any import oracledb
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class OracleIntrospection(DatabaseIntrospection):
|
||||
# Associating any OBJECTVAR instances with GeometryField. This won't work
|
||||
# right on Oracle objects that aren't MDSYS.SDO_GEOMETRY, but it is the
|
||||
# only object type supported within Django anyways.
|
||||
@cached_property
|
||||
def data_types_reverse(self):
|
||||
return {
|
||||
**super().data_types_reverse,
|
||||
oracledb.DB_TYPE_OBJECT: "GeometryField",
|
||||
}
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
with self.connection.cursor() as cursor:
|
||||
# Querying USER_SDO_GEOM_METADATA to get the SRID and dimension information.
|
||||
try:
|
||||
cursor.execute(
|
||||
'SELECT "DIMINFO", "SRID" FROM "USER_SDO_GEOM_METADATA" '
|
||||
'WHERE "TABLE_NAME"=%s AND "COLUMN_NAME"=%s',
|
||||
(table_name.upper(), description.name.upper()),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
except Exception as exc:
|
||||
raise Exception(
|
||||
"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"
|
||||
|
||||
# Getting the field parameters.
|
||||
field_params = {}
|
||||
dim, srid = row
|
||||
if srid != 4326:
|
||||
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
|
||||
return field_type, field_params
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the Oracle spatial
|
||||
backend.
|
||||
|
||||
It should be noted that Oracle Spatial does not have database tables
|
||||
named according to the OGC standard, so the closest analogs are used.
|
||||
For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns
|
||||
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
|
||||
|
||||
class OracleGeometryColumns(models.Model):
|
||||
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
|
||||
|
||||
table_name = models.CharField(max_length=32)
|
||||
column_name = models.CharField(max_length=1024)
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
geometry column.
|
||||
"""
|
||||
return "column_name"
|
||||
|
||||
|
||||
class OracleSpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"Maps to the Oracle MDSYS.CS_SRS table."
|
||||
|
||||
cs_name = models.CharField(max_length=68)
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
auth_srid = models.IntegerField()
|
||||
auth_name = models.CharField(max_length=256)
|
||||
wktext = models.CharField(max_length=2046)
|
||||
# Optional geometry representing the bounds of this coordinate
|
||||
# system. By default, all are NULL in the table.
|
||||
cs_bounds = models.PolygonField(null=True)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "CS_SRS"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.wktext
|
||||
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for Oracle Spatial.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
|
||||
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.oracle.adapter import OracleSpatialAdapter
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
|
||||
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"
|
||||
|
||||
|
||||
class SDOOperator(SpatialOperator):
|
||||
sql_template = "%(func)s(%(lhs)s, %(rhs)s) = 'TRUE'"
|
||||
|
||||
|
||||
class SDODWithin(SpatialOperator):
|
||||
sql_template = "SDO_WITHIN_DISTANCE(%(lhs)s, %(rhs)s, %%s) = 'TRUE'"
|
||||
|
||||
|
||||
class SDODisjoint(SpatialOperator):
|
||||
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)
|
||||
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]
|
||||
return super().as_sql(connection, lookup, template_params, sql_params[:-1])
|
||||
|
||||
|
||||
class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "oracle"
|
||||
oracle = True
|
||||
disallowed_aggregates = (models.Collect, models.Extent3D, models.MakeLine)
|
||||
|
||||
Adapter = OracleSpatialAdapter
|
||||
|
||||
extent = "SDO_AGGR_MBR"
|
||||
unionagg = "SDO_AGGR_UNION"
|
||||
|
||||
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",
|
||||
"FromWKB": "SDO_UTIL.FROM_WKBGEOMETRY",
|
||||
"FromWKT": "SDO_UTIL.FROM_WKTGEOMETRY",
|
||||
"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
|
||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||
# 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)"
|
||||
|
||||
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(),
|
||||
}
|
||||
|
||||
unsupported_functions = {
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"ClosestPoint",
|
||||
"ForcePolygonCW",
|
||||
"GeoHash",
|
||||
"GeometryDistance",
|
||||
"IsEmpty",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"Translate",
|
||||
}
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return super().geo_quote_name(name).upper()
|
||||
|
||||
def convert_extent(self, clob):
|
||||
if clob:
|
||||
# Generally, Oracle returns a polygon for the extent -- however,
|
||||
# it can return a single point if there's only one Point in the
|
||||
# table.
|
||||
ext_geom = GEOSGeometry(memoryview(clob.read()))
|
||||
gtype = str(ext_geom.geom_type)
|
||||
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":
|
||||
ll = ext_geom.coords[:2]
|
||||
ur = ll
|
||||
else:
|
||||
raise Exception(
|
||||
"Unexpected geometry type returned for extent: %s" % gtype
|
||||
)
|
||||
xmin, ymin = ll
|
||||
xmax, ymax = ur
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
else:
|
||||
return None
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the geometry database type for Oracle. Unlike other spatial
|
||||
backends, no stored procedure is necessary and it's the same for all
|
||||
geometry types.
|
||||
"""
|
||||
return "MDSYS.SDO_GEOMETRY"
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Return the distance parameters given the value and the lookup type.
|
||||
On Oracle, geometry columns with a geodetic coordinate system behave
|
||||
implicitly like a geography column, and thus meters will be used as
|
||||
the distance parameter on them.
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
dist_param = value.m
|
||||
else:
|
||||
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
|
||||
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
if value is None:
|
||||
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()
|
||||
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
|
||||
|
||||
return OracleGeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
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":
|
||||
return []
|
||||
return super().modify_insert_params(placeholder, params)
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
srid = expression.output_field.srid
|
||||
if srid == -1:
|
||||
srid = None
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if value is not None:
|
||||
geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
|
||||
if srid:
|
||||
geom.srid = srid
|
||||
return geom
|
||||
|
||||
return converter
|
||||
|
||||
def get_area_att_for_field(self, field):
|
||||
return "sq_m"
|
||||
@@ -0,0 +1,151 @@
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.db.backends.oracle.schema import DatabaseSchemaEditor
|
||||
from django.db.backends.utils import strip_quotes, truncate_name
|
||||
|
||||
|
||||
class OracleGISSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_geometry_metadata = """
|
||||
INSERT INTO USER_SDO_GEOM_METADATA
|
||||
("TABLE_NAME", "COLUMN_NAME", "DIMINFO", "SRID")
|
||||
VALUES (
|
||||
%(table)s,
|
||||
%(column)s,
|
||||
MDSYS.SDO_DIM_ARRAY(
|
||||
MDSYS.SDO_DIM_ELEMENT('LONG', %(dim0)s, %(dim2)s, %(tolerance)s),
|
||||
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_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"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.geometry_sql = []
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return self.connection.ops.geo_quote_name(name)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
return super().quote_value(value)
|
||||
|
||||
def _field_indexes_sql(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index:
|
||||
return [self._create_spatial_index_sql(model, field)]
|
||||
return super()._field_indexes_sql(model, field)
|
||||
|
||||
def column_sql(self, model, field, include_default=False):
|
||||
column_sql = super().column_sql(model, field, include_default)
|
||||
if isinstance(field, GeometryField):
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_geometry_metadata
|
||||
% {
|
||||
"table": self.geo_quote_name(model._meta.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,
|
||||
}
|
||||
)
|
||||
return column_sql
|
||||
|
||||
def create_model(self, model):
|
||||
super().create_model(model)
|
||||
self.run_geometry_sql()
|
||||
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
||||
def add_field(self, model, field):
|
||||
super().add_field(model, field)
|
||||
self.run_geometry_sql()
|
||||
|
||||
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),
|
||||
}
|
||||
)
|
||||
if field.spatial_index:
|
||||
self.execute(self._delete_spatial_index_sql(model, field))
|
||||
super().remove_field(model, field)
|
||||
|
||||
def run_geometry_sql(self):
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
|
||||
def _alter_field(
|
||||
self,
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=False,
|
||||
):
|
||||
super()._alter_field(
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
old_field_spatial_index = (
|
||||
isinstance(old_field, GeometryField) and old_field.spatial_index
|
||||
)
|
||||
new_field_spatial_index = (
|
||||
isinstance(new_field, GeometryField) and new_field.spatial_index
|
||||
)
|
||||
if not old_field_spatial_index and new_field_spatial_index:
|
||||
self.execute(self._create_spatial_index_sql(model, new_field))
|
||||
elif old_field_spatial_index and not new_field_spatial_index:
|
||||
self.execute(self._delete_spatial_index_sql(model, old_field))
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def _create_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
return self.sql_add_spatial_index % {
|
||||
"index": self.quote_name(index_name),
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
|
||||
def _delete_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
return self._delete_index_sql(model, index_name)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db.backends.postgis.pgraster import to_pgraster
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.db.backends.postgresql.psycopg_any import sql
|
||||
|
||||
|
||||
class PostGISAdapter:
|
||||
def __init__(self, obj, geography=False):
|
||||
"""
|
||||
Initialize on the spatial object.
|
||||
"""
|
||||
self.is_geometry = isinstance(obj, (GEOSGeometry, PostGISAdapter))
|
||||
|
||||
# Getting the WKB (in string form, to allow easy pickling of
|
||||
# the adaptor) and the SRID from the geometry or raster.
|
||||
if self.is_geometry:
|
||||
self.ewkb = bytes(obj.ewkb)
|
||||
else:
|
||||
self.ewkb = to_pgraster(obj)
|
||||
|
||||
self.srid = obj.srid
|
||||
self.geography = geography
|
||||
|
||||
def __conform__(self, proto):
|
||||
"""Does the given protocol conform to what Psycopg2 expects?"""
|
||||
from psycopg2.extensions import ISQLQuote
|
||||
|
||||
if proto == ISQLQuote:
|
||||
return self
|
||||
else:
|
||||
raise Exception(
|
||||
"Error implementing psycopg2 protocol. Is psycopg2 installed?"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, PostGISAdapter) and self.ewkb == other.ewkb
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ewkb)
|
||||
|
||||
def __str__(self):
|
||||
return self.getquoted().decode()
|
||||
|
||||
@classmethod
|
||||
def _fix_polygon(cls, poly):
|
||||
return poly
|
||||
|
||||
def getquoted(self):
|
||||
"""
|
||||
Return a properly quoted string for use in PostgreSQL/PostGIS.
|
||||
"""
|
||||
if self.is_geometry:
|
||||
# Psycopg will figure out whether to use E'\\000' or '\000'.
|
||||
return b"%s(%s)" % (
|
||||
b"ST_GeogFromWKB" if self.geography else b"ST_GeomFromEWKB",
|
||||
sql.quote(self.ewkb).encode(),
|
||||
)
|
||||
else:
|
||||
# For rasters, add explicit type cast to WKB string.
|
||||
return b"'%s'::raster" % self.ewkb.hex().encode()
|
||||
@@ -0,0 +1,161 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from django.db.backends.base.base import NO_DB_ALIAS
|
||||
from django.db.backends.postgresql.base import DatabaseWrapper as PsycopgDatabaseWrapper
|
||||
from django.db.backends.postgresql.features import (
|
||||
DatabaseFeatures as PsycopgDatabaseFeatures,
|
||||
)
|
||||
from django.db.backends.postgresql.introspection import (
|
||||
DatabaseIntrospection as PsycopgDatabaseIntrospection,
|
||||
)
|
||||
from django.db.backends.postgresql.operations import (
|
||||
DatabaseOperations as PsycopgDatabaseOperations,
|
||||
)
|
||||
from django.db.backends.postgresql.psycopg_any import is_psycopg3
|
||||
|
||||
from .adapter import PostGISAdapter
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import PostGISIntrospection
|
||||
from .operations import PostGISOperations
|
||||
from .schema import PostGISSchemaEditor
|
||||
|
||||
if is_psycopg3:
|
||||
from psycopg.adapt import Dumper
|
||||
from psycopg.pq import Format
|
||||
from psycopg.types import TypeInfo
|
||||
from psycopg.types.string import TextBinaryLoader, TextLoader
|
||||
|
||||
class GeometryType:
|
||||
pass
|
||||
|
||||
class GeographyType:
|
||||
pass
|
||||
|
||||
class RasterType:
|
||||
pass
|
||||
|
||||
class BaseTextDumper(Dumper):
|
||||
def dump(self, obj):
|
||||
# Return bytes as hex for text formatting
|
||||
return obj.ewkb.hex().encode()
|
||||
|
||||
class BaseBinaryDumper(Dumper):
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return obj.ewkb
|
||||
|
||||
@lru_cache
|
||||
def postgis_adapters(geo_oid, geog_oid, raster_oid):
|
||||
class BaseDumper(Dumper):
|
||||
def __init_subclass__(cls, base_dumper):
|
||||
super().__init_subclass__()
|
||||
|
||||
cls.GeometryDumper = type(
|
||||
"GeometryDumper", (base_dumper,), {"oid": geo_oid}
|
||||
)
|
||||
cls.GeographyDumper = type(
|
||||
"GeographyDumper", (base_dumper,), {"oid": geog_oid}
|
||||
)
|
||||
cls.RasterDumper = type(
|
||||
"RasterDumper", (BaseTextDumper,), {"oid": raster_oid}
|
||||
)
|
||||
|
||||
def get_key(self, obj, format):
|
||||
if obj.is_geometry:
|
||||
return GeographyType if obj.geography else GeometryType
|
||||
else:
|
||||
return RasterType
|
||||
|
||||
def upgrade(self, obj, format):
|
||||
if obj.is_geometry:
|
||||
if obj.geography:
|
||||
return self.GeographyDumper(GeographyType)
|
||||
else:
|
||||
return self.GeometryDumper(GeometryType)
|
||||
else:
|
||||
return self.RasterDumper(RasterType)
|
||||
|
||||
def dump(self, obj):
|
||||
raise NotImplementedError
|
||||
|
||||
class PostGISTextDumper(BaseDumper, base_dumper=BaseTextDumper):
|
||||
pass
|
||||
|
||||
class PostGISBinaryDumper(BaseDumper, base_dumper=BaseBinaryDumper):
|
||||
format = Format.BINARY
|
||||
|
||||
return PostGISTextDumper, PostGISBinaryDumper
|
||||
|
||||
|
||||
class DatabaseWrapper(PsycopgDatabaseWrapper):
|
||||
SchemaEditorClass = PostGISSchemaEditor
|
||||
features_class = DatabaseFeatures
|
||||
ops_class = PostGISOperations
|
||||
introspection_class = PostGISIntrospection
|
||||
|
||||
_type_infos = {
|
||||
"geometry": {},
|
||||
"geography": {},
|
||||
"raster": {},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get("alias", "") == NO_DB_ALIAS:
|
||||
# Don't initialize PostGIS-specific stuff for non-db connections.
|
||||
self.features_class = PsycopgDatabaseFeatures
|
||||
self.ops_class = PsycopgDatabaseOperations
|
||||
self.introspection_class = PsycopgDatabaseIntrospection
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def prepare_database(self):
|
||||
super().prepare_database()
|
||||
# Check that postgis extension is installed.
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM pg_extension WHERE extname = %s", ["postgis"])
|
||||
if bool(cursor.fetchone()):
|
||||
return
|
||||
cursor.execute("CREATE EXTENSION IF NOT EXISTS postgis")
|
||||
if is_psycopg3:
|
||||
# Ensure adapters are registers if PostGIS is used within this
|
||||
# connection.
|
||||
self.register_geometry_adapters(self.connection, True)
|
||||
|
||||
def get_new_connection(self, conn_params):
|
||||
connection = super().get_new_connection(conn_params)
|
||||
if is_psycopg3:
|
||||
self.register_geometry_adapters(connection)
|
||||
return connection
|
||||
|
||||
if is_psycopg3:
|
||||
|
||||
def _register_type(self, pg_connection, typename):
|
||||
registry = self._type_infos[typename]
|
||||
try:
|
||||
info = registry[self.alias]
|
||||
except KeyError:
|
||||
info = TypeInfo.fetch(pg_connection, typename)
|
||||
registry[self.alias] = info
|
||||
|
||||
if info: # Can be None if the type does not exist (yet).
|
||||
info.register(pg_connection)
|
||||
pg_connection.adapters.register_loader(info.oid, TextLoader)
|
||||
pg_connection.adapters.register_loader(info.oid, TextBinaryLoader)
|
||||
|
||||
return info.oid if info else None
|
||||
|
||||
def register_geometry_adapters(self, pg_connection, clear_caches=False):
|
||||
if clear_caches:
|
||||
for typename in self._type_infos:
|
||||
self._type_infos[typename].pop(self.alias, None)
|
||||
|
||||
geo_oid = self._register_type(pg_connection, "geometry")
|
||||
geog_oid = self._register_type(pg_connection, "geography")
|
||||
raster_oid = self._register_type(pg_connection, "raster")
|
||||
|
||||
PostGISTextDumper, PostGISBinaryDumper = postgis_adapters(
|
||||
geo_oid, geog_oid, raster_oid
|
||||
)
|
||||
pg_connection.adapters.register_dumper(PostGISAdapter, PostGISTextDumper)
|
||||
pg_connection.adapters.register_dumper(PostGISAdapter, PostGISBinaryDumper)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
PostGIS to GDAL conversion constant definitions
|
||||
"""
|
||||
|
||||
# Lookup to convert pixel type values from GDAL to PostGIS
|
||||
GDAL_TO_POSTGIS = [None, 4, 6, 5, 8, 7, 10, 11, None, None, None, None]
|
||||
|
||||
# Lookup to convert pixel type values from PostGIS to GDAL
|
||||
POSTGIS_TO_GDAL = [1, 1, 1, 3, 1, 3, 2, 5, 4, None, 6, 7, None, None]
|
||||
|
||||
# Struct pack structure for raster header, the raster header has the
|
||||
# following structure:
|
||||
#
|
||||
# Endianness, PostGIS raster version, number of bands, scale, origin,
|
||||
# skew, srid, width, and height.
|
||||
#
|
||||
# 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"
|
||||
|
||||
# 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,
|
||||
]
|
||||
|
||||
# Size of the packed value in bytes for different numerical types.
|
||||
# This is needed to cut chunks of band data out of PostGIS raster strings
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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
|
||||
BANDTYPE_PIXTYPE_MASK = 0x0F
|
||||
BANDTYPE_FLAG_HASNODATA = 1 << 6
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.postgresql.features import (
|
||||
DatabaseFeatures as PsycopgDatabaseFeatures,
|
||||
)
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, PsycopgDatabaseFeatures):
|
||||
supports_geography = True
|
||||
supports_3d_storage = True
|
||||
supports_3d_functions = True
|
||||
supports_raster = True
|
||||
supports_empty_geometries = True
|
||||
empty_intersection_returns_none = False
|
||||
@@ -0,0 +1,71 @@
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.postgresql.introspection import DatabaseIntrospection
|
||||
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
def get_field_type(self, data_type, description):
|
||||
if not self.postgis_oid_lookup:
|
||||
# Query PostgreSQL's pg_type table to determine the OID integers
|
||||
# for the PostGIS data types used in reverse lookup (the integers
|
||||
# may be different across versions). To prevent unnecessary
|
||||
# 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')"
|
||||
)
|
||||
self.postgis_oid_lookup = dict(cursor.fetchall())
|
||||
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):
|
||||
"""
|
||||
The geometry type OID used by PostGIS does not indicate the particular
|
||||
type of field that a geometry column is (e.g., whether it's a
|
||||
PointField or a PolygonField). Thus, this routine queries the PostGIS
|
||||
metadata tables to determine the geometry type.
|
||||
"""
|
||||
with self.connection.cursor() as cursor:
|
||||
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),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
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 srid != 4326:
|
||||
field_params["srid"] = srid
|
||||
if dim != 2:
|
||||
field_params["dim"] = dim
|
||||
return field_type, field_params
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the PostGIS backend.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
from django.db import models
|
||||
|
||||
|
||||
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)
|
||||
f_geometry_column = models.CharField(max_length=256)
|
||||
coord_dimension = models.IntegerField()
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
type = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "geometry_columns"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return "%s.%s - %dD %s field (SRID: %d)" % (
|
||||
self.f_table_name,
|
||||
self.f_geometry_column,
|
||||
self.coord_dimension,
|
||||
self.type,
|
||||
self.srid,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "f_table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
geometry column.
|
||||
"""
|
||||
return "f_geometry_column"
|
||||
|
||||
|
||||
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()
|
||||
srtext = models.CharField(max_length=2048)
|
||||
proj4text = models.CharField(max_length=2048)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "spatial_ref_sys"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.srtext
|
||||
@@ -0,0 +1,423 @@
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.db.models import GeometryField, RasterField
|
||||
from django.contrib.gis.gdal import GDALRaster
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import NotSupportedError, ProgrammingError
|
||||
from django.db.backends.postgresql.operations import DatabaseOperations
|
||||
from django.db.backends.postgresql.psycopg_any import is_psycopg3
|
||||
from django.db.models import Func, Value
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.version import get_version_tuple
|
||||
|
||||
from .adapter import PostGISAdapter
|
||||
from .models import PostGISGeometryColumns, PostGISSpatialRefSys
|
||||
from .pgraster import from_pgraster
|
||||
|
||||
# Identifier to mark raster lookups as bilateral.
|
||||
BILATERAL = "bilateral"
|
||||
|
||||
|
||||
class PostGISOperator(SpatialOperator):
|
||||
def __init__(self, geography=False, raster=False, **kwargs):
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# geography type. Lookups that don't support geography will be cast to
|
||||
# geometry.
|
||||
self.geography = geography
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# raster type. Lookups that don't support raster will be converted to
|
||||
# polygons. If the raster argument is set to BILATERAL, then the
|
||||
# operator cannot handle mixed geom-raster lookups.
|
||||
self.raster = raster
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def as_sql(self, connection, lookup, template_params, *args):
|
||||
template_params = self.check_raster(lookup, template_params)
|
||||
template_params = self.check_geography(lookup, template_params)
|
||||
return super().as_sql(connection, lookup, template_params, *args)
|
||||
|
||||
def check_raster(self, lookup, template_params):
|
||||
spheroid = lookup.rhs_params and lookup.rhs_params[-1] == "spheroid"
|
||||
|
||||
# Check which input is a raster.
|
||||
lhs_is_raster = lookup.lhs.field.geom_type == "RASTER"
|
||||
rhs_is_raster = isinstance(lookup.rhs, GDALRaster)
|
||||
|
||||
# Look for band indices and inject them if provided.
|
||||
if lookup.band_lhs is not None and lhs_is_raster:
|
||||
if not self.func:
|
||||
raise ValueError(
|
||||
"Band indices are not allowed for this operator, it works on bbox "
|
||||
"only."
|
||||
)
|
||||
template_params["lhs"] = "%s, %s" % (
|
||||
template_params["lhs"],
|
||||
lookup.band_lhs,
|
||||
)
|
||||
|
||||
if lookup.band_rhs is not None and rhs_is_raster:
|
||||
if not self.func:
|
||||
raise ValueError(
|
||||
"Band indices are not allowed for this operator, it works on bbox "
|
||||
"only."
|
||||
)
|
||||
template_params["rhs"] = "%s, %s" % (
|
||||
template_params["rhs"],
|
||||
lookup.band_rhs,
|
||||
)
|
||||
|
||||
# Convert rasters to polygons if necessary.
|
||||
if not self.raster or spheroid:
|
||||
# Operators without raster support.
|
||||
if lhs_is_raster:
|
||||
template_params["lhs"] = "ST_Polygon(%s)" % template_params["lhs"]
|
||||
if rhs_is_raster:
|
||||
template_params["rhs"] = "ST_Polygon(%s)" % template_params["rhs"]
|
||||
elif self.raster == BILATERAL:
|
||||
# Operators with raster support but don't support mixed (rast-geom)
|
||||
# lookups.
|
||||
if lhs_is_raster and not rhs_is_raster:
|
||||
template_params["lhs"] = "ST_Polygon(%s)" % template_params["lhs"]
|
||||
elif rhs_is_raster and not lhs_is_raster:
|
||||
template_params["rhs"] = "ST_Polygon(%s)" % template_params["rhs"]
|
||||
|
||||
return template_params
|
||||
|
||||
def check_geography(self, lookup, template_params):
|
||||
"""Convert geography fields to geometry types, if necessary."""
|
||||
if lookup.lhs.output_field.geography and not self.geography:
|
||||
template_params["lhs"] += "::geometry"
|
||||
return template_params
|
||||
|
||||
|
||||
class ST_Polygon(Func):
|
||||
function = "ST_Polygon"
|
||||
|
||||
def __init__(self, expr):
|
||||
super().__init__(expr)
|
||||
expr = self.source_expressions[0]
|
||||
if isinstance(expr, Value) and not expr._output_field_or_none:
|
||||
self.source_expressions[0] = Value(
|
||||
expr.value, output_field=RasterField(srid=expr.value.srid)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return GeometryField(srid=self.source_expressions[0].field.srid)
|
||||
|
||||
|
||||
class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "postgis"
|
||||
postgis = True
|
||||
geom_func_prefix = "ST_"
|
||||
|
||||
Adapter = PostGISAdapter
|
||||
|
||||
collect = geom_func_prefix + "Collect"
|
||||
extent = geom_func_prefix + "Extent"
|
||||
extent3d = geom_func_prefix + "3DExtent"
|
||||
length3d = geom_func_prefix + "3DLength"
|
||||
makeline = geom_func_prefix + "MakeLine"
|
||||
perimeter3d = geom_func_prefix + "3DPerimeter"
|
||||
unionagg = geom_func_prefix + "Union"
|
||||
|
||||
gis_operators = {
|
||||
"bbcontains": PostGISOperator(op="~", raster=True),
|
||||
"bboverlaps": PostGISOperator(op="&&", geography=True, raster=True),
|
||||
"contained": PostGISOperator(op="@", raster=True),
|
||||
"overlaps_left": PostGISOperator(op="&<", raster=BILATERAL),
|
||||
"overlaps_right": PostGISOperator(op="&>", raster=BILATERAL),
|
||||
"overlaps_below": PostGISOperator(op="&<|"),
|
||||
"overlaps_above": PostGISOperator(op="|&>"),
|
||||
"left": PostGISOperator(op="<<"),
|
||||
"right": PostGISOperator(op=">>"),
|
||||
"strictly_below": PostGISOperator(op="<<|"),
|
||||
"strictly_above": PostGISOperator(op="|>>"),
|
||||
"same_as": PostGISOperator(op="~=", raster=BILATERAL),
|
||||
"exact": PostGISOperator(op="~=", raster=BILATERAL), # alias of same_as
|
||||
"contains": PostGISOperator(func="ST_Contains", raster=BILATERAL),
|
||||
"contains_properly": PostGISOperator(
|
||||
func="ST_ContainsProperly", raster=BILATERAL
|
||||
),
|
||||
"coveredby": PostGISOperator(
|
||||
func="ST_CoveredBy", geography=True, raster=BILATERAL
|
||||
),
|
||||
"covers": PostGISOperator(func="ST_Covers", geography=True, raster=BILATERAL),
|
||||
"crosses": PostGISOperator(func="ST_Crosses"),
|
||||
"disjoint": PostGISOperator(func="ST_Disjoint", raster=BILATERAL),
|
||||
"equals": PostGISOperator(func="ST_Equals"),
|
||||
"intersects": PostGISOperator(
|
||||
func="ST_Intersects", geography=True, raster=BILATERAL
|
||||
),
|
||||
"overlaps": PostGISOperator(func="ST_Overlaps", raster=BILATERAL),
|
||||
"relate": PostGISOperator(func="ST_Relate"),
|
||||
"touches": PostGISOperator(func="ST_Touches", raster=BILATERAL),
|
||||
"within": PostGISOperator(func="ST_Within", raster=BILATERAL),
|
||||
"dwithin": PostGISOperator(func="ST_DWithin", geography=True, raster=BILATERAL),
|
||||
}
|
||||
|
||||
unsupported_functions = set()
|
||||
|
||||
select = "%s" if is_psycopg3 else "%s::bytea"
|
||||
|
||||
select_extent = None
|
||||
|
||||
@cached_property
|
||||
def function_names(self):
|
||||
function_names = {
|
||||
"AsWKB": "ST_AsBinary",
|
||||
"AsWKT": "ST_AsText",
|
||||
"BoundingCircle": "ST_MinimumBoundingCircle",
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
"NumPoints": "ST_NPoints",
|
||||
}
|
||||
return function_names
|
||||
|
||||
@cached_property
|
||||
def spatial_version(self):
|
||||
"""Determine the version of the PostGIS library."""
|
||||
# Trying to get the PostGIS version because the function
|
||||
# signatures will depend on the version used. The cost
|
||||
# here is a database query to determine the version, which
|
||||
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
|
||||
# comprising user-supplied values for the major, minor, and
|
||||
# subminor revision of PostGIS.
|
||||
if hasattr(settings, "POSTGIS_VERSION"):
|
||||
version = settings.POSTGIS_VERSION
|
||||
else:
|
||||
# Run a basic query to check the status of the connection so we're
|
||||
# sure we only raise the error below if the problem comes from
|
||||
# PostGIS and not from PostgreSQL itself (see #24862).
|
||||
self._get_postgis_func("version")
|
||||
|
||||
try:
|
||||
vtup = self.postgis_version_tuple()
|
||||
except ProgrammingError:
|
||||
raise ImproperlyConfigured(
|
||||
'Cannot determine PostGIS version for database "%s" '
|
||||
'using command "SELECT postgis_lib_version()". '
|
||||
"GeoDjango requires at least PostGIS version 3.1. "
|
||||
"Was the database created from a spatial database "
|
||||
"template?" % self.connection.settings_dict["NAME"]
|
||||
)
|
||||
version = vtup[1:]
|
||||
return version
|
||||
|
||||
def convert_extent(self, box):
|
||||
"""
|
||||
Return a 4-tuple extent for the `Extent` aggregate by converting
|
||||
the bounding box text returned by PostGIS (`box` argument), for
|
||||
example: "BOX(-90.0 30.0, -85.0 40.0)".
|
||||
"""
|
||||
if box is None:
|
||||
return None
|
||||
ll, ur = box[4:-1].split(",")
|
||||
xmin, ymin = map(float, ll.split())
|
||||
xmax, ymax = map(float, ur.split())
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def convert_extent3d(self, box3d):
|
||||
"""
|
||||
Return a 6-tuple extent for the `Extent3D` aggregate by converting
|
||||
the 3d bounding-box text returned by PostGIS (`box3d` argument), for
|
||||
example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
|
||||
"""
|
||||
if box3d is None:
|
||||
return None
|
||||
ll, ur = box3d[6:-1].split(",")
|
||||
xmin, ymin, zmin = map(float, ll.split())
|
||||
xmax, ymax, zmax = map(float, ur.split())
|
||||
return (xmin, ymin, zmin, xmax, ymax, zmax)
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the database field type for the given spatial field.
|
||||
"""
|
||||
if f.geom_type == "RASTER":
|
||||
return "raster"
|
||||
|
||||
# Type-based geometries.
|
||||
# TODO: Support 'M' extension.
|
||||
if f.dim == 3:
|
||||
geom_type = f.geom_type + "Z"
|
||||
else:
|
||||
geom_type = f.geom_type
|
||||
if f.geography:
|
||||
if f.srid != 4326:
|
||||
raise NotSupportedError(
|
||||
"PostGIS only supports geography columns with an SRID of 4326."
|
||||
)
|
||||
|
||||
return "geography(%s,%d)" % (geom_type, f.srid)
|
||||
else:
|
||||
return "geometry(%s,%d)" % (geom_type, f.srid)
|
||||
|
||||
def get_distance(self, f, dist_val, lookup_type):
|
||||
"""
|
||||
Retrieve the distance parameters for the given geometry field,
|
||||
distance lookup value, and the distance lookup type.
|
||||
|
||||
This is the most complex implementation of the spatial backends due to
|
||||
what is supported on geodetic geometry columns vs. what's available on
|
||||
projected geometry columns. In addition, it has to take into account
|
||||
the geography column type.
|
||||
"""
|
||||
# Getting the distance parameter
|
||||
value = dist_val[0]
|
||||
|
||||
# Shorthand boolean flags.
|
||||
geodetic = f.geodetic(self.connection)
|
||||
geography = f.geography
|
||||
|
||||
if isinstance(value, Distance):
|
||||
if geography:
|
||||
dist_param = value.m
|
||||
elif geodetic:
|
||||
if lookup_type == "dwithin":
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are "
|
||||
"allowed on geographic DWithin queries."
|
||||
)
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
# Assuming the distance is in the units of the field.
|
||||
dist_param = value
|
||||
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value, compiler):
|
||||
"""
|
||||
Provide a proper substitution value for Geometries or rasters that are
|
||||
not in the SRID of the field. Specifically, this routine will
|
||||
substitute in the ST_Transform() function call.
|
||||
"""
|
||||
transform_func = self.spatial_function_name("Transform")
|
||||
if hasattr(value, "as_sql"):
|
||||
if value.field.srid == f.srid:
|
||||
placeholder = "%s"
|
||||
else:
|
||||
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
|
||||
return placeholder
|
||||
|
||||
# Get the srid for this object
|
||||
if value is None:
|
||||
value_srid = None
|
||||
else:
|
||||
value_srid = value.srid
|
||||
|
||||
# Adding Transform() to the SQL placeholder if the value srid
|
||||
# is not equal to the field srid.
|
||||
if value_srid is None or value_srid == f.srid:
|
||||
placeholder = "%s"
|
||||
else:
|
||||
placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
|
||||
|
||||
return placeholder
|
||||
|
||||
def _get_postgis_func(self, func):
|
||||
"""
|
||||
Helper routine for calling PostGIS functions and returning their result.
|
||||
"""
|
||||
# Close out the connection. See #9437.
|
||||
with self.connection.temporary_connection() as cursor:
|
||||
cursor.execute("SELECT %s()" % func)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def postgis_geos_version(self):
|
||||
"Return the version of the GEOS library used with PostGIS."
|
||||
return self._get_postgis_func("postgis_geos_version")
|
||||
|
||||
def postgis_lib_version(self):
|
||||
"Return the version number of the PostGIS library used with PostgreSQL."
|
||||
return self._get_postgis_func("postgis_lib_version")
|
||||
|
||||
def postgis_proj_version(self):
|
||||
"""Return the version of the PROJ library used with PostGIS."""
|
||||
return self._get_postgis_func("postgis_proj_version")
|
||||
|
||||
def postgis_version(self):
|
||||
"Return PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func("postgis_version")
|
||||
|
||||
def postgis_full_version(self):
|
||||
"Return PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func("postgis_full_version")
|
||||
|
||||
def postgis_version_tuple(self):
|
||||
"""
|
||||
Return the PostGIS version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
version = self.postgis_lib_version()
|
||||
return (version,) + get_version_tuple(version)
|
||||
|
||||
def proj_version_tuple(self):
|
||||
"""
|
||||
Return the version of PROJ used by PostGIS as a tuple of the
|
||||
major, minor, and subminor release numbers.
|
||||
"""
|
||||
proj_regex = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
||||
proj_ver_str = self.postgis_proj_version()
|
||||
m = proj_regex.search(proj_ver_str)
|
||||
if m:
|
||||
return tuple(map(int, m.groups()))
|
||||
else:
|
||||
raise Exception("Could not determine PROJ version from PostGIS.")
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
if agg_name == "Extent3D":
|
||||
return self.extent3d
|
||||
else:
|
||||
return self.geom_func_prefix + agg_name
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
return PostGISGeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
return PostGISSpatialRefSys
|
||||
|
||||
def parse_raster(self, value):
|
||||
"""Convert a PostGIS HEX String into a dict readable by GDALRaster."""
|
||||
return from_pgraster(value)
|
||||
|
||||
def distance_expr_for_lookup(self, lhs, rhs, **kwargs):
|
||||
return super().distance_expr_for_lookup(
|
||||
self._normalize_distance_lookup_arg(lhs),
|
||||
self._normalize_distance_lookup_arg(rhs),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_distance_lookup_arg(arg):
|
||||
is_raster = (
|
||||
arg.field.geom_type == "RASTER"
|
||||
if hasattr(arg, "field")
|
||||
else isinstance(arg, GDALRaster)
|
||||
)
|
||||
return ST_Polygon(arg) if is_raster else arg
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if isinstance(value, str): # Coming from hex strings.
|
||||
value = value.encode("ascii")
|
||||
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"
|
||||
@@ -0,0 +1,152 @@
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def pack(structure, data):
|
||||
"""
|
||||
Pack data into hex string with little endian format.
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
def chunk(data, index):
|
||||
"""
|
||||
Split a string into two parts at the input index.
|
||||
"""
|
||||
return data[:index], data[index:]
|
||||
|
||||
|
||||
def from_pgraster(data):
|
||||
"""
|
||||
Convert a PostGIS HEX String into a dictionary.
|
||||
"""
|
||||
if data is None:
|
||||
return
|
||||
|
||||
# Split raster header from data
|
||||
header, data = chunk(data, 122)
|
||||
header = unpack(POSTGIS_HEADER_STRUCTURE, header)
|
||||
|
||||
# Parse band data
|
||||
bands = []
|
||||
pixeltypes = []
|
||||
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 = pixeltype_with_flags & BANDTYPE_PIXTYPE_MASK
|
||||
|
||||
# Convert datatype from PostGIS to GDAL & get pack type and size
|
||||
pixeltype = POSTGIS_TO_GDAL[pixeltype]
|
||||
pack_type = GDAL_TO_STRUCT[pixeltype]
|
||||
pack_size = 2 * STRUCT_SIZE[pack_type]
|
||||
|
||||
# Parse band nodata value. The nodata value is part of the
|
||||
# PGRaster string even if the nodata flag is True, so it always
|
||||
# has to be chunked off the data string.
|
||||
nodata, data = chunk(data, pack_size)
|
||||
nodata = unpack(pack_type, nodata)[0]
|
||||
|
||||
# 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)}
|
||||
|
||||
# Set the nodata value if the nodata flag is set.
|
||||
if pixeltype_with_flags & BANDTYPE_FLAG_HASNODATA:
|
||||
band_result["nodata_value"] = nodata
|
||||
|
||||
# Append band data to band list
|
||||
bands.append(band_result)
|
||||
|
||||
# Store pixeltype of this band in pixeltypes array
|
||||
pixeltypes.append(pixeltype)
|
||||
|
||||
# Check that all bands have the same pixeltype.
|
||||
# This is required by GDAL. PostGIS rasters could have different pixeltypes
|
||||
# for bands of the same raster.
|
||||
if len(set(pixeltypes)) != 1:
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def to_pgraster(rast):
|
||||
"""
|
||||
Convert a GDALRaster into PostGIS Raster format.
|
||||
"""
|
||||
# Prepare the raster header data as a tuple. The first two numbers are
|
||||
# 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,
|
||||
)
|
||||
|
||||
# Pack raster header.
|
||||
result = pack(POSTGIS_HEADER_STRUCTURE, rasterheader)
|
||||
|
||||
for band in rast.bands:
|
||||
# The PostGIS raster band header has exactly two elements, a 8BUI byte
|
||||
# and the nodata value.
|
||||
#
|
||||
# The 8BUI stores both the PostGIS pixel data type and a nodata flag.
|
||||
# It is composed as the datatype with BANDTYPE_FLAG_HASNODATA (1 << 6)
|
||||
# for existing nodata values:
|
||||
# 8BUI_VALUE = PG_PIXEL_TYPE (0-11) | BANDTYPE_FLAG_HASNODATA
|
||||
#
|
||||
# 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()]
|
||||
|
||||
# Get band pixel type in PostGIS notation
|
||||
pixeltype = GDAL_TO_POSTGIS[band.datatype()]
|
||||
|
||||
# Set the nodata flag
|
||||
if band.nodata_value is not None:
|
||||
pixeltype |= BANDTYPE_FLAG_HASNODATA
|
||||
|
||||
# Pack band header
|
||||
bandheader = pack(structure, (pixeltype, band.nodata_value or 0))
|
||||
|
||||
# Add packed header and band data to result
|
||||
result += bandheader + band.data(as_memoryview=True)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,124 @@
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
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)"
|
||||
|
||||
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):
|
||||
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"):
|
||||
return super()._create_index_sql(model, fields=fields, **kwargs)
|
||||
|
||||
return self._create_spatial_index_sql(model, fields[0], **kwargs)
|
||||
|
||||
def _alter_column_type_sql(
|
||||
self, table, old_field, new_field, new_type, old_collation, new_collation
|
||||
):
|
||||
"""
|
||||
Special case when dimension changed.
|
||||
"""
|
||||
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, old_collation, new_collation
|
||||
)
|
||||
|
||||
if old_field.dim == 2 and new_field.dim == 3:
|
||||
sql_alter = self.sql_alter_column_to_3d
|
||||
elif old_field.dim == 3 and new_field.dim == 2:
|
||||
sql_alter = self.sql_alter_column_to_2d
|
||||
else:
|
||||
sql_alter = self.sql_alter_column_type
|
||||
return (
|
||||
(
|
||||
sql_alter
|
||||
% {
|
||||
"column": self.quote_name(new_field.column),
|
||||
"type": new_type,
|
||||
"collation": "",
|
||||
},
|
||||
[],
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def _alter_field(
|
||||
self,
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=False,
|
||||
):
|
||||
super()._alter_field(
|
||||
model,
|
||||
old_field,
|
||||
new_field,
|
||||
old_type,
|
||||
new_type,
|
||||
old_db_params,
|
||||
new_db_params,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
old_field_spatial_index = (
|
||||
isinstance(old_field, GeometryField) and old_field.spatial_index
|
||||
)
|
||||
new_field_spatial_index = (
|
||||
isinstance(new_field, GeometryField) and new_field.spatial_index
|
||||
)
|
||||
if not old_field_spatial_index and new_field_spatial_index:
|
||||
self.execute(self._create_spatial_index_sql(model, new_field))
|
||||
elif old_field_spatial_index and not new_field_spatial_index:
|
||||
self.execute(self._delete_spatial_index_sql(model, old_field))
|
||||
|
||||
def _create_spatial_index_name(self, model, field):
|
||||
return self._create_index_name(model._meta.db_table, [field.column], "_id")
|
||||
|
||||
def _create_spatial_index_sql(self, model, field, **kwargs):
|
||||
expressions = None
|
||||
opclasses = None
|
||||
fields = [field]
|
||||
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
|
||||
elif field.dim > 2 and not field.geography:
|
||||
# Use "nd" ops which are fast on multidimensional cases
|
||||
opclasses = [self.geom_index_ops_nd]
|
||||
if not (name := kwargs.get("name")):
|
||||
name = self._create_spatial_index_name(model, field)
|
||||
|
||||
return super()._create_index_sql(
|
||||
model,
|
||||
fields=fields,
|
||||
name=name,
|
||||
using=" USING %s" % self.geom_index_type,
|
||||
opclasses=opclasses,
|
||||
expressions=expressions,
|
||||
)
|
||||
|
||||
def _delete_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
return self._delete_index_sql(model, index_name)
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
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)
|
||||
@@ -0,0 +1,79 @@
|
||||
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 .client import SpatiaLiteClient
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import SpatiaLiteIntrospection
|
||||
from .operations import SpatiaLiteOperations
|
||||
from .schema import SpatialiteSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(SQLiteDatabaseWrapper):
|
||||
SchemaEditorClass = SpatialiteSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
client_class = SpatiaLiteClient
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = SpatiaLiteIntrospection
|
||||
ops_class = SpatiaLiteOperations
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Trying to find the location of the SpatiaLite library.
|
||||
# Here we are figuring out the path to the SpatiaLite library
|
||||
# (`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
|
||||
]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_new_connection(self, conn_params):
|
||||
conn = super().get_new_connection(conn_params)
|
||||
# Enabling extension loading on the SQLite connection.
|
||||
try:
|
||||
conn.enable_load_extension(True)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
"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):
|
||||
raise ImproperlyConfigured(
|
||||
"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)
|
||||
)
|
||||
return conn
|
||||
|
||||
def prepare_database(self):
|
||||
super().prepare_database()
|
||||
# Check if spatial metadata have been initialized in the database
|
||||
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)")
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.db.backends.sqlite3.client import DatabaseClient
|
||||
|
||||
|
||||
class SpatiaLiteClient(DatabaseClient):
|
||||
executable_name = "spatialite"
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.sqlite3.features import (
|
||||
DatabaseFeatures as SQLiteDatabaseFeatures,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures):
|
||||
can_alter_geometry_field = False # Not implemented
|
||||
supports_3d_storage = True
|
||||
|
||||
@cached_property
|
||||
def supports_area_geodetic(self):
|
||||
return bool(self.connection.ops.geom_lib_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",
|
||||
},
|
||||
}
|
||||
)
|
||||
return skips
|
||||
@@ -0,0 +1,82 @@
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.sqlite3.introspection import (
|
||||
DatabaseIntrospection,
|
||||
FlexibleFieldLookupDict,
|
||||
)
|
||||
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
class SpatiaLiteIntrospection(DatabaseIntrospection):
|
||||
data_types_reverse = GeoFlexibleFieldLookupDict()
|
||||
|
||||
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),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
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.
|
||||
ogr_type = row[2]
|
||||
if isinstance(ogr_type, int) and ogr_type > 1000:
|
||||
# SpatiaLite uses SFSQL 1.2 offsets 1000 (Z), 2000 (M), and
|
||||
# 3000 (ZM) to indicate the presence of higher dimensional
|
||||
# coordinates (M not yet supported by Django).
|
||||
ogr_type = ogr_type % 1000 + OGRGeomType.wkb25bit
|
||||
field_type = OGRGeomType(ogr_type).django
|
||||
|
||||
# Getting any GeometryField keyword arguments that are not the default.
|
||||
dim = row[0]
|
||||
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
|
||||
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,),
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
constraints["%s__spatial__index" % row[0]] = {
|
||||
"columns": [row[0]],
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"foreign_key": None,
|
||||
"check": False,
|
||||
"index": True,
|
||||
}
|
||||
return constraints
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
The GeometryColumns and SpatialRefSys models for the SpatiaLite backend.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
|
||||
from django.db import models
|
||||
|
||||
|
||||
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")
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "geometry_columns"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return "%s.%s - %dD %s field (SRID: %d)" % (
|
||||
self.f_table_name,
|
||||
self.f_geometry_column,
|
||||
self.coord_dimension,
|
||||
self.type,
|
||||
self.srid,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def table_name_col(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature table
|
||||
name.
|
||||
"""
|
||||
return "f_table_name"
|
||||
|
||||
@classmethod
|
||||
def geom_col_name(cls):
|
||||
"""
|
||||
Return the name of the metadata column used to store the feature
|
||||
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()
|
||||
ref_sys_name = models.CharField(max_length=256)
|
||||
proj4text = models.CharField(max_length=2048)
|
||||
srtext = models.CharField(max_length=2048)
|
||||
|
||||
class Meta:
|
||||
app_label = "gis"
|
||||
db_table = "spatial_ref_sys"
|
||||
managed = False
|
||||
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.srtext
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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.spatialite.adapter import SpatiaLiteAdapter
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.sqlite3.operations import DatabaseOperations
|
||||
from django.utils.functional import cached_property
|
||||
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
|
||||
|
||||
|
||||
class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "spatialite"
|
||||
spatialite = True
|
||||
|
||||
Adapter = SpatiaLiteAdapter
|
||||
|
||||
collect = "Collect"
|
||||
extent = "Extent"
|
||||
makeline = "MakeLine"
|
||||
unionagg = "GUnion"
|
||||
|
||||
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"),
|
||||
# Returns true if B's bounding box completely contains A's bounding box.
|
||||
"contained": SpatialOperator(func="MbrWithin"),
|
||||
# Returns true if A's bounding box completely contains B's bounding box.
|
||||
"bbcontains": SpatialOperator(func="MbrContains"),
|
||||
# Returns true if A's bounding box overlaps B's bounding box.
|
||||
"bboverlaps": SpatialOperator(func="MbrOverlaps"),
|
||||
# These are implemented here as synonyms for Equals
|
||||
"same_as": SpatialiteNullCheckOperator(func="Equals"),
|
||||
"exact": SpatialiteNullCheckOperator(func="Equals"),
|
||||
# Distance predicates
|
||||
"dwithin": SpatialOperator(func="PtDistWithin"),
|
||||
}
|
||||
|
||||
disallowed_aggregates = (models.Extent3D,)
|
||||
|
||||
select = "CAST (AsEWKB(%s) AS BLOB)"
|
||||
|
||||
function_names = {
|
||||
"AsWKB": "St_AsBinary",
|
||||
"BoundingCircle": "GEOSMinimumBoundingCircle",
|
||||
"ForcePolygonCW": "ST_ForceLHR",
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
"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 = {"GeometryDistance", "IsEmpty", "MemSize"}
|
||||
if not self.geom_lib_version():
|
||||
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
|
||||
if self.spatial_version < (5, 1):
|
||||
unsupported |= {"BoundingCircle"}
|
||||
return unsupported
|
||||
|
||||
@cached_property
|
||||
def spatial_version(self):
|
||||
"""Determine the version of the SpatiaLite library."""
|
||||
try:
|
||||
version = self.spatialite_version_tuple()[1:]
|
||||
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"],)
|
||||
) from exc
|
||||
if version < (4, 3, 0):
|
||||
raise ImproperlyConfigured("GeoDjango supports SpatiaLite 4.3.0 and above.")
|
||||
return version
|
||||
|
||||
def convert_extent(self, box):
|
||||
"""
|
||||
Convert the polygon data received from SpatiaLite to min/max values.
|
||||
"""
|
||||
if box is None:
|
||||
return None
|
||||
shell = GEOSGeometry(box).shell
|
||||
xmin, ymin = shell[0][:2]
|
||||
xmax, ymax = shell[2][:2]
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return None because geometry columns are added via the
|
||||
`AddGeometryColumn` stored procedure on SpatiaLite.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Return the distance parameters for the given geometry field,
|
||||
lookup value, and lookup type.
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
if lookup_type == "dwithin":
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are allowed on "
|
||||
"geographic DWithin queries."
|
||||
)
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def _get_spatialite_func(self, func):
|
||||
"""
|
||||
Helper routine for calling SpatiaLite functions and returning
|
||||
their result.
|
||||
Any error occurring in this method should be handled by the caller.
|
||||
"""
|
||||
cursor = self.connection._cursor()
|
||||
try:
|
||||
cursor.execute("SELECT %s" % func)
|
||||
row = cursor.fetchone()
|
||||
finally:
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
def geos_version(self):
|
||||
"Return the version of GEOS used by SpatiaLite as a string."
|
||||
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()")
|
||||
|
||||
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()
|
||||
|
||||
def spatialite_version(self):
|
||||
"Return the SpatiaLite library version as a string."
|
||||
return self._get_spatialite_func("spatialite_version()")
|
||||
|
||||
def spatialite_version_tuple(self):
|
||||
"""
|
||||
Return the SpatiaLite version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
version = self.spatialite_version()
|
||||
return (version,) + get_version_tuple(version)
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
"""
|
||||
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()
|
||||
return getattr(self, agg_name)
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
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):
|
||||
geom_class = expression.output_field.geom_class
|
||||
read = wkb_r().read
|
||||
|
||||
def converter(value, expression, connection):
|
||||
return None if value is None else GEOSGeometryBase(read(value), geom_class)
|
||||
|
||||
return converter
|
||||
@@ -0,0 +1,194 @@
|
||||
from django.db import DatabaseError
|
||||
from django.db.backends.sqlite3.schema import DatabaseSchemaEditor
|
||||
|
||||
|
||||
class SpatialiteSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_geometry_column = (
|
||||
"SELECT AddGeometryColumn(%(table)s, %(column)s, %(srid)s, "
|
||||
"%(geom_type)s, %(dim)s, %(null)s)"
|
||||
)
|
||||
sql_add_spatial_index = "SELECT CreateSpatialIndex(%(table)s, %(column)s)"
|
||||
sql_drop_spatial_index = "DROP TABLE idx_%(table)s_%(column)s"
|
||||
sql_recover_geometry_metadata = (
|
||||
"SELECT RecoverGeometryColumn(%(table)s, %(column)s, %(srid)s, "
|
||||
"%(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_update_geometry_columns = (
|
||||
"UPDATE %(geom_table)s SET f_table_name = %(new_table)s "
|
||||
"WHERE f_table_name = %(old_table)s"
|
||||
)
|
||||
|
||||
geometry_tables = [
|
||||
"geometry_columns",
|
||||
"geometry_columns_auth",
|
||||
"geometry_columns_time",
|
||||
"geometry_columns_statistics",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.geometry_sql = []
|
||||
|
||||
def geo_quote_name(self, name):
|
||||
return self.connection.ops.geo_quote_name(name)
|
||||
|
||||
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
|
||||
% {
|
||||
"table": self.geo_quote_name(model._meta.db_table),
|
||||
"column": self.geo_quote_name(field.column),
|
||||
"srid": field.srid,
|
||||
"geom_type": self.geo_quote_name(field.geom_type),
|
||||
"dim": field.dim,
|
||||
"null": int(not field.null),
|
||||
}
|
||||
)
|
||||
|
||||
if field.spatial_index:
|
||||
self.geometry_sql.append(
|
||||
self.sql_add_spatial_index
|
||||
% {
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
return None, None
|
||||
|
||||
def remove_geometry_metadata(self, model, field):
|
||||
self.execute(
|
||||
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
|
||||
% {
|
||||
"table": model._meta.db_table,
|
||||
"column": field.column,
|
||||
}
|
||||
)
|
||||
|
||||
def create_model(self, model):
|
||||
super().create_model(model)
|
||||
# Create geometry columns
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
|
||||
def delete_model(self, model, **kwargs):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
# Drop spatial metadata (dropping the table does not automatically remove them)
|
||||
for field in model._meta.local_fields:
|
||||
if isinstance(field, GeometryField):
|
||||
self.remove_geometry_metadata(model, field)
|
||||
# Make sure all geom stuff is gone
|
||||
for geom_table in self.geometry_tables:
|
||||
try:
|
||||
self.execute(
|
||||
self.sql_discard_geometry_columns
|
||||
% {
|
||||
"geom_table": geom_table,
|
||||
"table": self.quote_name(model._meta.db_table),
|
||||
}
|
||||
)
|
||||
except DatabaseError:
|
||||
pass
|
||||
super().delete_model(model, **kwargs)
|
||||
|
||||
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)
|
||||
for sql in self.geometry_sql:
|
||||
self.execute(sql)
|
||||
self.geometry_sql = []
|
||||
else:
|
||||
super().add_field(model, field)
|
||||
|
||||
def remove_field(self, model, field):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
# NOTE: If the field is a geometry field, the table is just recreated,
|
||||
# the parent's remove_field can't be used cause it will skip the
|
||||
# recreation if the field does not have a database type. Geometry fields
|
||||
# do not have a db type cause they are added and removed via stored
|
||||
# procedures.
|
||||
if isinstance(field, GeometryField):
|
||||
self._remake_table(model, delete_field=field)
|
||||
else:
|
||||
super().remove_field(model, field)
|
||||
|
||||
def alter_db_table(self, model, old_db_table, new_db_table):
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
|
||||
if old_db_table == new_db_table or (
|
||||
self.connection.features.ignores_table_name_case
|
||||
and old_db_table.lower() == new_db_table.lower()
|
||||
):
|
||||
return
|
||||
# Remove geometry-ness from temp table
|
||||
for field in model._meta.local_fields:
|
||||
if isinstance(field, GeometryField):
|
||||
self.execute(
|
||||
self.sql_remove_geometry_metadata
|
||||
% {
|
||||
"table": self.quote_name(old_db_table),
|
||||
"column": self.quote_name(field.column),
|
||||
}
|
||||
)
|
||||
# Alter table
|
||||
super().alter_db_table(model, old_db_table, new_db_table)
|
||||
# Repoint any straggler names
|
||||
for geom_table in self.geometry_tables:
|
||||
try:
|
||||
self.execute(
|
||||
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),
|
||||
}
|
||||
)
|
||||
except DatabaseError:
|
||||
pass
|
||||
# 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)
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
A collection of utility routines and classes used by the spatial
|
||||
backends.
|
||||
"""
|
||||
|
||||
|
||||
class SpatialOperator:
|
||||
"""
|
||||
Class encapsulating the behavior specific to a GIS operation (used by lookups).
|
||||
"""
|
||||
|
||||
sql_template = None
|
||||
|
||||
def __init__(self, op=None, func=None):
|
||||
self.op = op
|
||||
self.func = func
|
||||
|
||||
@property
|
||||
def default_template(self):
|
||||
if self.func:
|
||||
return "%(func)s(%(lhs)s, %(rhs)s)"
|
||||
else:
|
||||
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})
|
||||
return sql_template % template_params, sql_params
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.db.models import * # NOQA isort:skip
|
||||
from django.db.models import __all__ as models_all # isort:skip
|
||||
import django.contrib.gis.db.models.functions # NOQA
|
||||
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,
|
||||
)
|
||||
|
||||
__all__ = models_all + aggregates_all
|
||||
__all__ += [
|
||||
"GeometryCollectionField",
|
||||
"GeometryField",
|
||||
"LineStringField",
|
||||
"MultiLineStringField",
|
||||
"MultiPointField",
|
||||
"MultiPolygonField",
|
||||
"PointField",
|
||||
"PolygonField",
|
||||
"RasterField",
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
from django.contrib.gis.db.models.fields import (
|
||||
ExtentField,
|
||||
GeometryCollectionField,
|
||||
GeometryField,
|
||||
LineStringField,
|
||||
)
|
||||
from django.db.models import Aggregate, Func, Value
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
__all__ = ["Collect", "Extent", "Extent3D", "MakeLine", "Union"]
|
||||
|
||||
|
||||
class GeoAggregate(Aggregate):
|
||||
function = None
|
||||
is_extent = False
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return self.output_field_class(self.source_expressions[0].output_field.srid)
|
||||
|
||||
def as_sql(self, compiler, connection, function=None, **extra_context):
|
||||
# this will be called again in parent, but it's needed now - before
|
||||
# we get the spatial_aggregate_name
|
||||
connection.ops.check_expression_support(self)
|
||||
return super().as_sql(
|
||||
compiler,
|
||||
connection,
|
||||
function=function or connection.ops.spatial_aggregate_name(self.name),
|
||||
**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)
|
||||
clone = self.copy()
|
||||
source_expressions = self.get_source_expressions()
|
||||
source_expressions.pop() # Don't wrap filters with SDOAGGRTYPE().
|
||||
spatial_type_expr = Func(
|
||||
*source_expressions,
|
||||
Value(tolerance),
|
||||
function="SDOAGGRTYPE",
|
||||
output_field=self.output_field,
|
||||
)
|
||||
source_expressions = [spatial_type_expr, self.filter]
|
||||
clone.set_source_expressions(source_expressions)
|
||||
return clone.as_sql(compiler, connection, **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
|
||||
):
|
||||
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
||||
for field in c.get_source_fields():
|
||||
if not hasattr(field, "geom_type"):
|
||||
raise ValueError(
|
||||
"Geospatial aggregates only allowed on geometry fields."
|
||||
)
|
||||
return c
|
||||
|
||||
|
||||
class Collect(GeoAggregate):
|
||||
name = "Collect"
|
||||
output_field_class = GeometryCollectionField
|
||||
|
||||
|
||||
class Extent(GeoAggregate):
|
||||
name = "Extent"
|
||||
is_extent = "2D"
|
||||
|
||||
def __init__(self, expression, **extra):
|
||||
super().__init__(expression, output_field=ExtentField(), **extra)
|
||||
|
||||
def convert_value(self, value, expression, connection):
|
||||
return connection.ops.convert_extent(value)
|
||||
|
||||
|
||||
class Extent3D(GeoAggregate):
|
||||
name = "Extent3D"
|
||||
is_extent = "3D"
|
||||
|
||||
def __init__(self, expression, **extra):
|
||||
super().__init__(expression, output_field=ExtentField(), **extra)
|
||||
|
||||
def convert_value(self, value, expression, connection):
|
||||
return connection.ops.convert_extent3d(value)
|
||||
|
||||
|
||||
class MakeLine(GeoAggregate):
|
||||
name = "MakeLine"
|
||||
output_field_class = LineStringField
|
||||
|
||||
|
||||
class Union(GeoAggregate):
|
||||
name = "Union"
|
||||
output_field_class = GeometryField
|
||||
@@ -0,0 +1,434 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
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,
|
||||
)
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Field
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Local cache of the spatial_ref_sys table, which holds SRID data for each
|
||||
# spatial database alias. This cache exists so that the database isn't queried
|
||||
# for SRID info each time a distance query is constructed.
|
||||
_srid_cache = defaultdict(dict)
|
||||
|
||||
|
||||
SRIDCacheEntry = namedtuple(
|
||||
"SRIDCacheEntry", ["units", "units_name", "spheroid", "geodetic"]
|
||||
)
|
||||
|
||||
|
||||
def get_srid_info(srid, connection):
|
||||
"""
|
||||
Return the units, unit name, and spheroid WKT associated with the
|
||||
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
|
||||
table for the given database connection. These results are cached.
|
||||
"""
|
||||
from django.contrib.gis.gdal import SpatialReference
|
||||
|
||||
try:
|
||||
# The SpatialRefSys model for the spatial backend.
|
||||
SpatialRefSys = connection.ops.spatial_ref_sys()
|
||||
except NotImplementedError:
|
||||
SpatialRefSys = None
|
||||
|
||||
alias, get_srs = (
|
||||
(
|
||||
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)
|
||||
units, units_name = srs.units
|
||||
_srid_cache[alias][srid] = SRIDCacheEntry(
|
||||
units=units,
|
||||
units_name=units_name,
|
||||
spheroid='SPHEROID["%s",%s,%s]'
|
||||
% (srs["spheroid"], srs.semi_major, srs.inverse_flattening),
|
||||
geodetic=srs.geographic,
|
||||
)
|
||||
|
||||
return _srid_cache[alias][srid]
|
||||
|
||||
|
||||
class BaseSpatialField(Field):
|
||||
"""
|
||||
The Base GIS Field.
|
||||
|
||||
It's used as a base class for GeometryField and RasterField. Defines
|
||||
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
|
||||
|
||||
def __init__(self, verbose_name=None, srid=4326, spatial_index=True, **kwargs):
|
||||
"""
|
||||
The initialization function for base spatial fields. Takes the following
|
||||
as keyword arguments:
|
||||
|
||||
srid:
|
||||
The spatial reference system identifier, an OGC standard.
|
||||
Defaults to 4326 (WGS84).
|
||||
|
||||
spatial_index:
|
||||
Indicates whether to create a spatial index. Defaults to True.
|
||||
Set this instead of 'db_index' for geographic fields since index
|
||||
creation is different for geometry columns.
|
||||
"""
|
||||
|
||||
# Setting the index flag with the value of the `spatial_index` keyword.
|
||||
self.spatial_index = spatial_index
|
||||
|
||||
# Setting the SRID and getting the units. Unit information must be
|
||||
# easily available in the field instance for distance queries.
|
||||
self.srid = srid
|
||||
|
||||
# Setting the verbose_name keyword argument with the positional
|
||||
# first parameter, so this works like normal fields.
|
||||
kwargs["verbose_name"] = verbose_name
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
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
|
||||
if self.spatial_index is not True:
|
||||
kwargs["spatial_index"] = self.spatial_index
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
return connection.ops.geo_db_type(self)
|
||||
|
||||
def spheroid(self, connection):
|
||||
return get_srid_info(self.srid, connection).spheroid
|
||||
|
||||
def units(self, connection):
|
||||
return get_srid_info(self.srid, connection).units
|
||||
|
||||
def units_name(self, connection):
|
||||
return get_srid_info(self.srid, connection).units_name
|
||||
|
||||
def geodetic(self, connection):
|
||||
"""
|
||||
Return true if this field's SRID corresponds with a coordinate
|
||||
system that uses non-projected units (e.g., latitude/longitude).
|
||||
"""
|
||||
return get_srid_info(self.srid, connection).geodetic
|
||||
|
||||
def get_placeholder(self, value, compiler, connection):
|
||||
"""
|
||||
Return the placeholder for the spatial column for the
|
||||
given value.
|
||||
"""
|
||||
return connection.ops.get_geom_placeholder(self, value, compiler)
|
||||
|
||||
def get_srid(self, obj):
|
||||
"""
|
||||
Return the default SRID for the given geometry or raster, taking into
|
||||
account the SRID set for the field. For example, if the input geometry
|
||||
or raster doesn't have an SRID, then the SRID of the field will be
|
||||
returned.
|
||||
"""
|
||||
srid = obj.srid # SRID of given geometry.
|
||||
if srid is None or self.srid == -1 or (srid == -1 and self.srid != -1):
|
||||
return self.srid
|
||||
else:
|
||||
return srid
|
||||
|
||||
def get_db_prep_value(self, value, connection, *args, **kwargs):
|
||||
if value is None:
|
||||
return None
|
||||
return connection.ops.Adapter(
|
||||
super().get_db_prep_value(value, connection, *args, **kwargs),
|
||||
**(
|
||||
{"geography": True}
|
||||
if self.geography and connection.features.supports_geography
|
||||
else {}
|
||||
),
|
||||
)
|
||||
|
||||
def get_raster_prep_value(self, value, is_candidate):
|
||||
"""
|
||||
Return a GDALRaster if conversion is successful, otherwise return None.
|
||||
"""
|
||||
if isinstance(value, gdal.GDALRaster):
|
||||
return value
|
||||
elif is_candidate:
|
||||
try:
|
||||
return gdal.GDALRaster(value)
|
||||
except GDALException:
|
||||
pass
|
||||
elif isinstance(value, dict):
|
||||
try:
|
||||
return gdal.GDALRaster(value)
|
||||
except GDALException:
|
||||
raise ValueError(
|
||||
"Couldn't create spatial object from lookup value '%s'." % value
|
||||
)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
obj = super().get_prep_value(value)
|
||||
if obj is None:
|
||||
return None
|
||||
# When the input is not a geometry or raster, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(obj, GEOSGeometry):
|
||||
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__"
|
||||
)
|
||||
# Try to convert the input to raster.
|
||||
raster = self.get_raster_prep_value(obj, is_candidate)
|
||||
|
||||
if raster:
|
||||
obj = raster
|
||||
elif is_candidate:
|
||||
try:
|
||||
obj = GEOSGeometry(obj)
|
||||
except (GEOSException, GDALException):
|
||||
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__
|
||||
)
|
||||
|
||||
# Assigning the SRID value.
|
||||
obj.srid = self.get_srid(obj)
|
||||
return obj
|
||||
|
||||
|
||||
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."
|
||||
)
|
||||
form_class = forms.GeometryField
|
||||
# The OpenGIS Geometry name.
|
||||
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,
|
||||
):
|
||||
"""
|
||||
The initialization function for geometry fields. In addition to the
|
||||
parameters from BaseSpatialField, it takes the following as keyword
|
||||
arguments:
|
||||
|
||||
dim:
|
||||
The number of dimensions for this geometry. Defaults to 2.
|
||||
|
||||
extent:
|
||||
Customize the extent, in a 4-tuple of WGS 84 coordinates, for the
|
||||
geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults
|
||||
to (-180.0, -90.0, 180.0, 90.0).
|
||||
|
||||
tolerance:
|
||||
Define the tolerance, in meters, to use for the geometry field
|
||||
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
|
||||
"""
|
||||
# Setting the dimension of the geometry field.
|
||||
self.dim = dim
|
||||
|
||||
# Is this a geography rather than a geometry column?
|
||||
self.geography = geography
|
||||
|
||||
# Oracle-specific private attributes for creating the entry in
|
||||
# `USER_SDO_GEOM_METADATA`
|
||||
self._extent = extent
|
||||
self._tolerance = tolerance
|
||||
|
||||
super().__init__(verbose_name=verbose_name, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
# Include kwargs if they're not the default values.
|
||||
if self.dim != 2:
|
||||
kwargs["dim"] = self.dim
|
||||
if self.geography is not False:
|
||||
kwargs["geography"] = self.geography
|
||||
if self._extent != (-180.0, -90.0, 180.0, 90.0):
|
||||
kwargs["extent"] = self._extent
|
||||
if self._tolerance != 0.05:
|
||||
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),
|
||||
)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
"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)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
def select_format(self, compiler, sql, params):
|
||||
"""
|
||||
Return the selection format string, depending on the requirements
|
||||
of the spatial backend. For example, Oracle and MySQL require custom
|
||||
selection formats in order to retrieve geometries in OGC WKB.
|
||||
"""
|
||||
if not compiler.query.subquery:
|
||||
return compiler.connection.ops.select % sql, params
|
||||
return sql, params
|
||||
|
||||
|
||||
# The OpenGIS Geometry Type Fields
|
||||
class PointField(GeometryField):
|
||||
geom_type = "POINT"
|
||||
geom_class = Point
|
||||
form_class = forms.PointField
|
||||
description = _("Point")
|
||||
|
||||
|
||||
class LineStringField(GeometryField):
|
||||
geom_type = "LINESTRING"
|
||||
geom_class = LineString
|
||||
form_class = forms.LineStringField
|
||||
description = _("Line string")
|
||||
|
||||
|
||||
class PolygonField(GeometryField):
|
||||
geom_type = "POLYGON"
|
||||
geom_class = Polygon
|
||||
form_class = forms.PolygonField
|
||||
description = _("Polygon")
|
||||
|
||||
|
||||
class MultiPointField(GeometryField):
|
||||
geom_type = "MULTIPOINT"
|
||||
geom_class = MultiPoint
|
||||
form_class = forms.MultiPointField
|
||||
description = _("Multi-point")
|
||||
|
||||
|
||||
class MultiLineStringField(GeometryField):
|
||||
geom_type = "MULTILINESTRING"
|
||||
geom_class = MultiLineString
|
||||
form_class = forms.MultiLineStringField
|
||||
description = _("Multi-line string")
|
||||
|
||||
|
||||
class MultiPolygonField(GeometryField):
|
||||
geom_type = "MULTIPOLYGON"
|
||||
geom_class = MultiPolygon
|
||||
form_class = forms.MultiPolygonField
|
||||
description = _("Multi polygon")
|
||||
|
||||
|
||||
class GeometryCollectionField(GeometryField):
|
||||
geom_type = "GEOMETRYCOLLECTION"
|
||||
geom_class = GeometryCollection
|
||||
form_class = forms.GeometryCollectionField
|
||||
description = _("Geometry collection")
|
||||
|
||||
|
||||
class ExtentField(Field):
|
||||
"Used as a return value from an extent aggregate"
|
||||
|
||||
description = _("Extent Aggregate Field")
|
||||
|
||||
def get_internal_type(self):
|
||||
return "ExtentField"
|
||||
|
||||
def select_format(self, compiler, sql, params):
|
||||
select = compiler.connection.ops.select_extent
|
||||
return select % sql if select else sql, params
|
||||
|
||||
|
||||
class RasterField(BaseSpatialField):
|
||||
"""
|
||||
Raster field for GeoDjango -- evaluates into GDALRaster objects.
|
||||
"""
|
||||
|
||||
description = _("Raster Field")
|
||||
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."
|
||||
)
|
||||
|
||||
def db_type(self, connection):
|
||||
self._check_connection(connection)
|
||||
return super().db_type(connection)
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return connection.ops.parse_raster(value)
|
||||
|
||||
def contribute_to_class(self, cls, name, **kwargs):
|
||||
super().contribute_to_class(cls, name, **kwargs)
|
||||
# Setup for lazy-instantiated Raster object. For large querysets, the
|
||||
# instantiation of all GDALRasters can potentially be expensive. This
|
||||
# delays the instantiation of the objects to the moment of evaluation
|
||||
# of the raster attribute.
|
||||
setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self))
|
||||
|
||||
def get_transform(self, name):
|
||||
from django.contrib.gis.db.models.lookups import RasterBandTransform
|
||||
|
||||
try:
|
||||
band_index = int(name)
|
||||
return type(
|
||||
"SpecificRasterBandTransform",
|
||||
(RasterBandTransform,),
|
||||
{"band_index": band_index},
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
return super().get_transform(name)
|
||||
@@ -0,0 +1,590 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
|
||||
from django.contrib.gis.db.models.sql import AreaField, DistanceField
|
||||
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,
|
||||
)
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
NUMERIC_TYPES = (int, float, Decimal)
|
||||
|
||||
|
||||
class GeoFuncMixin:
|
||||
function = None
|
||||
geom_param_pos = (0,)
|
||||
|
||||
def __init__(self, *expressions, **extra):
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
# Ensure that value expressions are geometric.
|
||||
for pos in self.geom_param_pos:
|
||||
expr = self.source_expressions[pos]
|
||||
if not isinstance(expr, Value):
|
||||
continue
|
||||
try:
|
||||
output_field = expr.output_field
|
||||
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 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)
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@cached_property
|
||||
def geo_field(self):
|
||||
return self.source_expressions[self.geom_param_pos[0]].field
|
||||
|
||||
def as_sql(self, compiler, connection, function=None, **extra_context):
|
||||
if self.function is None and function is None:
|
||||
function = connection.ops.spatial_function_name(self.name)
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
def resolve_expression(self, *args, **kwargs):
|
||||
res = super().resolve_expression(*args, **kwargs)
|
||||
if not self.geom_param_pos:
|
||||
return res
|
||||
|
||||
# Ensure that expressions are geometric.
|
||||
source_fields = res.get_source_fields()
|
||||
for pos in self.geom_param_pos:
|
||||
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__,
|
||||
)
|
||||
)
|
||||
|
||||
base_srid = res.geo_field.srid
|
||||
for pos in self.geom_param_pos[1:]:
|
||||
expr = res.source_expressions[pos]
|
||||
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)
|
||||
return res
|
||||
|
||||
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)
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GeoFunc(GeoFuncMixin, Func):
|
||||
pass
|
||||
|
||||
|
||||
class GeomOutputGeoFunc(GeoFunc):
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return GeometryField(srid=self.geo_field.srid)
|
||||
|
||||
|
||||
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()
|
||||
]
|
||||
)
|
||||
return copy.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions([*self.get_source_expressions(), tolerance])
|
||||
return clone.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Area(OracleToleranceMixin, GeoFunc):
|
||||
arity = 1
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
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."
|
||||
)
|
||||
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
|
||||
return self.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Azimuth(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class AsGeoJSON(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
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))
|
||||
options = 0
|
||||
if crs and bbox:
|
||||
options = 3
|
||||
elif bbox:
|
||||
options = 1
|
||||
elif crs:
|
||||
options = 2
|
||||
expressions.append(options)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
source_expressions = self.get_source_expressions()
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions(source_expressions[:1])
|
||||
return super(AsGeoJSON, clone).as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class AsGML(GeoFunc):
|
||||
geom_param_pos = (1,)
|
||||
output_field = TextField()
|
||||
|
||||
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))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
source_expressions = self.get_source_expressions()
|
||||
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"
|
||||
)
|
||||
return super(AsGML, clone).as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class AsKML(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, precision=8, **extra):
|
||||
expressions = [expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
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)
|
||||
)
|
||||
expressions = [
|
||||
expression,
|
||||
relative,
|
||||
self._handle_param(precision, "precision", int),
|
||||
]
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class AsWKB(GeoFunc):
|
||||
output_field = BinaryField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class AsWKT(GeoFunc):
|
||||
output_field = TextField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class BoundingCircle(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
def __init__(self, expression, num_seg=48, **extra):
|
||||
super().__init__(expression, num_seg, **extra)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions([self.get_source_expressions()[0]])
|
||||
return super(BoundingCircle, clone).as_sqlite(
|
||||
compiler, connection, **extra_context
|
||||
)
|
||||
|
||||
|
||||
class Centroid(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class ClosestPoint(GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class Difference(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class DistanceResultMixin:
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DistanceField(self.geo_field)
|
||||
|
||||
def source_is_geography(self):
|
||||
return self.geo_field.geography and self.geo_field.srid == 4326
|
||||
|
||||
|
||||
class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
geom_param_pos = (0, 1)
|
||||
spheroid = None
|
||||
|
||||
def __init__(self, expr1, expr2, spheroid=None, **extra):
|
||||
expressions = [expr1, expr2]
|
||||
if spheroid is not None:
|
||||
self.spheroid = self._handle_param(spheroid, "spheroid", bool)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
function = None
|
||||
expr2 = clone.source_expressions[1]
|
||||
geography = self.source_is_geography()
|
||||
if expr2.output_field.geography != geography:
|
||||
if isinstance(expr2, Value):
|
||||
expr2.output_field.geography = geography
|
||||
else:
|
||||
clone.source_expressions[1] = Cast(
|
||||
expr2,
|
||||
GeometryField(srid=expr2.output_field.srid, geography=geography),
|
||||
)
|
||||
|
||||
if not geography and self.geo_field.geodetic(connection):
|
||||
# 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")
|
||||
# Replace boolean param by the real spheroid of the base field
|
||||
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
|
||||
)
|
||||
|
||||
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))
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Envelope(GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class ForcePolygonCW(GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class FromWKB(GeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = ()
|
||||
|
||||
def __init__(self, expression, srid=0, **extra):
|
||||
expressions = [
|
||||
expression,
|
||||
self._handle_param(srid, "srid", int),
|
||||
]
|
||||
if "output_field" not in extra:
|
||||
extra["output_field"] = GeometryField(srid=srid)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
# Oracle doesn't support the srid parameter.
|
||||
source_expressions = self.get_source_expressions()
|
||||
clone = self.copy()
|
||||
clone.set_source_expressions(source_expressions[:1])
|
||||
return super(FromWKB, clone).as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class FromWKT(FromWKB):
|
||||
pass
|
||||
|
||||
|
||||
class GeoHash(GeoFunc):
|
||||
output_field = TextField()
|
||||
|
||||
def __init__(self, expression, precision=None, **extra):
|
||||
expressions = [expression]
|
||||
if precision is not None:
|
||||
expressions.append(self._handle_param(precision, "precision", int))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
def as_mysql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
# If no precision is provided, set it to the maximum.
|
||||
if len(clone.source_expressions) < 2:
|
||||
clone.source_expressions.append(Value(100))
|
||||
return clone.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class GeometryDistance(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
function = ""
|
||||
arg_joiner = " <-> "
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IsEmpty(GeoFuncMixin, Transform):
|
||||
lookup_name = "isempty"
|
||||
output_field = BooleanField()
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform):
|
||||
lookup_name = "isvalid"
|
||||
output_field = BooleanField()
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
sql, params = super().as_oracle(compiler, connection, **extra_context)
|
||||
return "CASE %s WHEN 'TRUE' THEN 1 ELSE 0 END" % sql, params
|
||||
|
||||
|
||||
class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
def __init__(self, expr1, spheroid=True, **extra):
|
||||
self.spheroid = spheroid
|
||||
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"
|
||||
)
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
function = None
|
||||
if self.source_is_geography():
|
||||
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")
|
||||
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
|
||||
)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
function = None
|
||||
if self.geo_field.geodetic(connection):
|
||||
function = "GeodesicLength" if self.spheroid else "GreatCircleLength"
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
|
||||
class LineLocatePoint(GeoFunc):
|
||||
output_field = FloatField()
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class MakeValid(GeomOutputGeoFunc):
|
||||
pass
|
||||
|
||||
|
||||
class MemSize(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class NumGeometries(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class NumPoints(GeoFunc):
|
||||
output_field = IntegerField()
|
||||
arity = 1
|
||||
|
||||
|
||||
class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||
arity = 1
|
||||
|
||||
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."
|
||||
)
|
||||
dim = min(f.dim for f in self.get_source_fields())
|
||||
if dim > 2:
|
||||
function = connection.ops.perimeter3d
|
||||
return super().as_sql(compiler, connection, function=function, **extra_context)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
if self.geo_field.geodetic(connection):
|
||||
raise NotSupportedError("Perimeter cannot use a non-projected field.")
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class PointOnSurface(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
class Reverse(GeoFunc):
|
||||
arity = 1
|
||||
|
||||
|
||||
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),
|
||||
]
|
||||
if z != 0.0:
|
||||
expressions.append(self._handle_param(z, "z", NUMERIC_TYPES))
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class SnapToGrid(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
|
||||
def __init__(self, expression, *args, **extra):
|
||||
nargs = len(args)
|
||||
expressions = [expression]
|
||||
if nargs in (1, 2):
|
||||
expressions.extend(
|
||||
[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]),
|
||||
]
|
||||
else:
|
||||
raise ValueError("Must provide 1, 2, or 4 arguments to `SnapToGrid`.")
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class SymDifference(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
|
||||
|
||||
class Transform(GeomOutputGeoFunc):
|
||||
def __init__(self, expression, srid, **extra):
|
||||
expressions = [
|
||||
expression,
|
||||
self._handle_param(srid, "srid", int),
|
||||
]
|
||||
if "output_field" not in extra:
|
||||
extra["output_field"] = GeometryField(srid=srid)
|
||||
super().__init__(*expressions, **extra)
|
||||
|
||||
|
||||
class Translate(Scale):
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
clone = self.copy()
|
||||
if len(self.source_expressions) < 4:
|
||||
# Always provide the z parameter for ST_Translate
|
||||
clone.source_expressions.append(Value(0))
|
||||
return super(Translate, clone).as_sqlite(compiler, connection, **extra_context)
|
||||
|
||||
|
||||
class Union(OracleToleranceMixin, GeomOutputGeoFunc):
|
||||
arity = 2
|
||||
geom_param_pos = (0, 1)
|
||||
@@ -0,0 +1,395 @@
|
||||
from django.contrib.gis.db.models.fields import BaseSpatialField
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db import NotSupportedError
|
||||
from django.db.models import Expression, Lookup, Transform
|
||||
from django.db.models.sql.query import Query
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
|
||||
class RasterBandTransform(Transform):
|
||||
def as_sql(self, compiler, connection):
|
||||
return compiler.compile(self.lhs)
|
||||
|
||||
|
||||
class GISLookup(Lookup):
|
||||
sql_template = None
|
||||
transform_func = None
|
||||
distance = False
|
||||
band_rhs = None
|
||||
band_lhs = None
|
||||
|
||||
def __init__(self, lhs, rhs):
|
||||
rhs, *self.rhs_params = rhs if isinstance(rhs, (list, tuple)) else [rhs]
|
||||
super().__init__(lhs, rhs)
|
||||
self.template_params = {}
|
||||
self.process_rhs_params()
|
||||
|
||||
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):
|
||||
self.process_band_indices()
|
||||
elif len(self.rhs_params) > 1:
|
||||
raise ValueError("Tuple too long for lookup %s." % self.lookup_name)
|
||||
elif isinstance(self.lhs, RasterBandTransform):
|
||||
self.process_band_indices(only_lhs=True)
|
||||
|
||||
def process_band_indices(self, only_lhs=False):
|
||||
"""
|
||||
Extract the lhs band index from the band transform class and the rhs
|
||||
band index from the input tuple.
|
||||
"""
|
||||
# PostGIS band indices are 1-based, so the band index needs to be
|
||||
# increased to be consistent with the GDALRaster band indices.
|
||||
if only_lhs:
|
||||
self.band_rhs = 1
|
||||
self.band_lhs = self.lhs.band_index + 1
|
||||
return
|
||||
|
||||
if isinstance(self.lhs, RasterBandTransform):
|
||||
self.band_lhs = self.lhs.band_index + 1
|
||||
else:
|
||||
self.band_lhs = 1
|
||||
|
||||
self.band_rhs, *self.rhs_params = self.rhs_params
|
||||
|
||||
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)])
|
||||
|
||||
def process_rhs(self, compiler, connection):
|
||||
if isinstance(self.rhs, Query):
|
||||
# If rhs is some Query, don't touch it.
|
||||
return super().process_rhs(compiler, connection)
|
||||
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
|
||||
)
|
||||
return placeholder % rhs, rhs_params
|
||||
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
# Unlike BuiltinLookup, the GIS get_rhs_op() implementation should return
|
||||
# an object (SpatialOperator) with an as_sql() method to allow for more
|
||||
# complex computations (where the lhs part can be mixed in).
|
||||
return connection.ops.gis_operators[self.lookup_name]
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
|
||||
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,
|
||||
}
|
||||
rhs_op = self.get_rhs_op(connection, rhs_sql)
|
||||
return rhs_op.as_sql(connection, self, template_params, sql_params)
|
||||
|
||||
|
||||
# ------------------
|
||||
# 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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
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"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class SameAsLookup(GISLookup):
|
||||
"""
|
||||
The "~=" operator is the "same as" operator. It tests actual geometric
|
||||
equality of two features. So if A and B are the same feature,
|
||||
vertex-by-vertex, the operator returns true.
|
||||
"""
|
||||
|
||||
lookup_name = "same_as"
|
||||
|
||||
|
||||
BaseSpatialField.register_lookup(SameAsLookup, "exact")
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class BBContainsLookup(GISLookup):
|
||||
"""
|
||||
The 'bbcontains' operator returns true if A's bounding box completely contains
|
||||
by B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "bbcontains"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class BBOverlapsLookup(GISLookup):
|
||||
"""
|
||||
The 'bboverlaps' operator returns true if A's bounding box overlaps B's
|
||||
bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "bboverlaps"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainedLookup(GISLookup):
|
||||
"""
|
||||
The 'contained' operator returns true if A's bounding box is completely contained
|
||||
by B's bounding box.
|
||||
"""
|
||||
|
||||
lookup_name = "contained"
|
||||
|
||||
|
||||
# ------------------
|
||||
# Geometry functions
|
||||
# ------------------
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainsLookup(GISLookup):
|
||||
lookup_name = "contains"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class ContainsProperlyLookup(GISLookup):
|
||||
lookup_name = "contains_properly"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CoveredByLookup(GISLookup):
|
||||
lookup_name = "coveredby"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CoversLookup(GISLookup):
|
||||
lookup_name = "covers"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class CrossesLookup(GISLookup):
|
||||
lookup_name = "crosses"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DisjointLookup(GISLookup):
|
||||
lookup_name = "disjoint"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class EqualsLookup(GISLookup):
|
||||
lookup_name = "equals"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IntersectsLookup(GISLookup):
|
||||
lookup_name = "intersects"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class OverlapsLookup(GISLookup):
|
||||
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}$")
|
||||
|
||||
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"):
|
||||
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)
|
||||
sql, params = super().process_rhs(compiler, connection)
|
||||
return sql, params + [pattern]
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class TouchesLookup(GISLookup):
|
||||
lookup_name = "touches"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class WithinLookup(GISLookup):
|
||||
lookup_name = "within"
|
||||
|
||||
|
||||
class DistanceLookupBase(GISLookup):
|
||||
distance = True
|
||||
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."
|
||||
)
|
||||
|
||||
# Check if the second parameter is a band index.
|
||||
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
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DWithinLookup(DistanceLookupBase):
|
||||
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)
|
||||
):
|
||||
raise NotSupportedError(
|
||||
"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
|
||||
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
|
||||
)
|
||||
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},
|
||||
params + dist_params,
|
||||
)
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceGTLookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_gt"
|
||||
op = ">"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceGTELookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_gte"
|
||||
op = ">="
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceLTLookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_lt"
|
||||
op = "<"
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class DistanceLTELookup(DistanceLookupFromFunction):
|
||||
lookup_name = "distance_lte"
|
||||
op = "<="
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
The SpatialProxy object allows for lazy-geometries and lazy-rasters. The proxy
|
||||
uses Python descriptors for instantiating and setting Geometry or Raster
|
||||
objects corresponding to geographic model fields.
|
||||
|
||||
Thanks to Robert Coup for providing this functionality (see #4322).
|
||||
"""
|
||||
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
|
||||
|
||||
class SpatialProxy(DeferredAttribute):
|
||||
def __init__(self, klass, field, load_func=None):
|
||||
"""
|
||||
Initialize on the given Geometry or Raster class (not an instance)
|
||||
and the corresponding field.
|
||||
"""
|
||||
self._klass = klass
|
||||
self._load_func = load_func or klass
|
||||
super().__init__(field)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Retrieve the geometry or raster, initializing it using the
|
||||
corresponding class specified during initialization and the value of
|
||||
the field. Currently, GEOS or OGR geometries as well as GDALRasters are
|
||||
supported.
|
||||
"""
|
||||
if instance is None:
|
||||
# Accessed on a class, not an instance
|
||||
return self
|
||||
|
||||
# Getting the value of the field.
|
||||
try:
|
||||
geo_value = instance.__dict__[self.field.attname]
|
||||
except KeyError:
|
||||
geo_value = super().__get__(instance, cls)
|
||||
|
||||
if isinstance(geo_value, self._klass):
|
||||
geo_obj = 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
|
||||
# contents, and the model's corresponding attribute is set.
|
||||
geo_obj = self._load_func(geo_value)
|
||||
setattr(instance, self.field.attname, geo_obj)
|
||||
return geo_obj
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Retrieve the proxied geometry or raster with the corresponding class
|
||||
specified during initialization.
|
||||
|
||||
To set geometries, use values of None, HEXEWKB, or WKT.
|
||||
To set rasters, use JSON or dict values.
|
||||
"""
|
||||
# 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
|
||||
# raster instance.
|
||||
pass
|
||||
elif isinstance(value, self._klass):
|
||||
# The geometry type must match that of the field -- unless the
|
||||
# general GeometryField is used.
|
||||
if value.srid is None:
|
||||
# Assigning the field SRID if the geometry has no SRID.
|
||||
value.srid = self.field.srid
|
||||
elif value is None or isinstance(value, (str, memoryview)):
|
||||
# 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))
|
||||
)
|
||||
|
||||
# Setting the objects dictionary with the value, and returning.
|
||||
instance.__dict__[self.field.attname] = value
|
||||
return value
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField
|
||||
|
||||
__all__ = [
|
||||
"AreaField",
|
||||
"DistanceField",
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
This module holds simple classes to convert geospatial values from the
|
||||
database.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.gis.measure import Area, Distance
|
||||
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.")
|
||||
return value
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if value is None:
|
||||
return
|
||||
area_att = connection.ops.get_area_att_for_field(self.geo_field)
|
||||
return getattr(value, area_att) if area_att else value
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return
|
||||
# If the database returns a Decimal, convert it to a float as expected
|
||||
# by the Python geometric objects.
|
||||
if isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
# If the units are known, convert value into area measure.
|
||||
area_att = connection.ops.get_area_att_for_field(self.geo_field)
|
||||
return Area(**{area_att: value}) if area_att else value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "AreaField"
|
||||
|
||||
|
||||
class DistanceField(models.FloatField):
|
||||
"Wrapper for Distance values."
|
||||
|
||||
def __init__(self, geo_field):
|
||||
super().__init__()
|
||||
self.geo_field = geo_field
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if isinstance(value, Distance):
|
||||
return value
|
||||
return super().get_prep_value(value)
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if not isinstance(value, Distance):
|
||||
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."
|
||||
)
|
||||
return getattr(value, distance_att)
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return
|
||||
distance_att = connection.ops.get_distance_att_for_field(self.geo_field)
|
||||
return Distance(**{distance_att: value}) if distance_att else value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DistanceField"
|
||||
Reference in New Issue
Block a user