start-pack
This commit is contained in:
commit
3e1fa59b3d
5723 changed files with 757971 additions and 0 deletions
|
|
@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue