identifier to mark raster lookups as bilateral 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 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 geomraster lookups check which input is a raster look for band indices and inject them if provided convert rasters to polygons if necessary operators without raster support operators with raster support but don t support mixed rastgeom lookups convert geography fields to geometry types if necessary if lookup lhs outputfield geography and not self geography templateparamslhs geometry return templateparams class stpolygonfunc function stpolygon def initself expr super initexpr expr self sourceexpressions0 if isinstanceexpr value and not expr outputfieldornone self sourceexpressions0 value expr value outputfieldrasterfieldsridexpr value srid cachedproperty def outputfieldself return geometryfieldsridself sourceexpressions0 field srid class postgisoperationsbasespatialoperations databaseoperations name postgis postgis true geomfuncprefix st adapter postgisadapter collect geomfuncprefix collect extent geomfuncprefix extent extent3d geomfuncprefix 3dextent length3d geomfuncprefix 3dlength makeline geomfuncprefix makeline perimeter3d geomfuncprefix 3dperimeter unionagg geomfuncprefix union gisoperators bbcontains postgisoperatorop rastertrue bboverlaps postgisoperatorop geographytrue rastertrue contained postgisoperatorop rastertrue overlapsleft postgisoperatorop rasterbilateral overlapsright postgisoperatorop rasterbilateral overlapsbelow postgisoperatorop overlapsabove postgisoperatorop left postgisoperatorop right postgisoperatorop strictlybelow postgisoperatorop strictlyabove postgisoperatorop sameas postgisoperatorop rasterbilateral exact postgisoperatorop rasterbilateral alias of sameas contains postgisoperatorfuncstcontains rasterbilateral containsproperly postgisoperator funcstcontainsproperly rasterbilateral coveredby postgisoperator funcstcoveredby geographytrue rasterbilateral covers postgisoperatorfuncstcovers geographytrue rasterbilateral crosses postgisoperatorfuncstcrosses disjoint postgisoperatorfuncstdisjoint rasterbilateral equals postgisoperatorfuncstequals intersects postgisoperator funcstintersects geographytrue rasterbilateral overlaps postgisoperatorfuncstoverlaps rasterbilateral relate postgisoperatorfuncstrelate touches postgisoperatorfuncsttouches rasterbilateral within postgisoperatorfuncstwithin rasterbilateral dwithin postgisoperatorfuncstdwithin geographytrue rasterbilateral unsupportedfunctions set select s if ispsycopg3 else s bytea selectextent none cachedproperty def functionnamesself functionnames aswkb stasbinary aswkt stastext boundingcircle stminimumboundingcircle fromwkb stgeomfromwkb fromwkt stgeomfromtext numpoints stnpoints return functionnames cachedproperty def spatialversionself 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 postgisversion with a 3tuple comprising usersupplied values for the major minor and subminor revision of postgis 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 return a 4tuple extent for the extent aggregate by converting the bounding box text returned by postgis box argument for example box90 0 30 0 85 0 40 0 return a 6tuple extent for the extent3d aggregate by converting the 3d boundingbox text returned by postgis box3d argument for example box3d90 0 30 0 1 85 0 40 0 2 return the database field type for the given spatial field typebased geometries todo support m extension 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 shorthand boolean flags assuming the distance is in the units of the field 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 sttransform function call get the srid for this object adding transform to the sql placeholder if the value srid is not equal to the field srid helper routine for calling postgis functions and returning their result close out the connection see 9437 return the version of the proj library used with postgis return self getpostgisfuncpostgisprojversion def postgisversionself return postgis version number and compiletime options return self getpostgisfuncpostgisversion def postgisfullversionself return postgis version number and compiletime options return self getpostgisfuncpostgisfullversion def postgisversiontupleself version self postgislibversion return version getversiontupleversion def projversiontupleself projregex re compilerd d d projverstr self postgisprojversion m projregex searchprojverstr if m return tuplemapint m groups else raise exceptioncould not determine proj version from postgis def spatialaggregatenameself aggname if aggname extent3d return self extent3d else return self geomfuncprefix aggname routines for getting the ogccompliant models def geometrycolumnsself return postgisgeometrycolumns def spatialrefsysself return postgisspatialrefsys def parserasterself value identifier to mark raster lookups as bilateral 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 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 check which input is a raster look for band indices and inject them if provided convert rasters to polygons if necessary operators without raster support operators with raster support but don t support mixed rast geom lookups convert geography fields to geometry types if necessary alias of same_as 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 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 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 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 return the database field type for the given spatial field type based geometries todo support m extension 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 shorthand boolean flags assuming the distance is in the units of the field 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 get the srid for this object adding transform to the sql placeholder if the value srid is not equal to the field srid helper routine for calling postgis functions and returning their result close out the connection see 9437 return the version of the proj library used with postgis return the postgis version as a tuple version string major minor subminor return the version of proj used by postgis as a tuple of the major minor and subminor release numbers routines for getting the ogc compliant models convert a postgis hex string into a dict readable by gdalraster coming from hex strings
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


BILATERAL = "bilateral"


class PostGISOperator(SpatialOperator):
    def __init__(self, geography=False, raster=False, **kwargs):
        
        
        
        self.geography = geography
        
        
        
        
        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"

        
        lhs_is_raster = lookup.lhs.field.geom_type == "RASTER"
        rhs_is_raster = isinstance(lookup.rhs, GDALRaster)

        
        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,
            )

        
        if not self.raster or spheroid:
            
            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:
            
            
            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):
        
        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),  
        "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):
        
        
        
        
        
        
        
        if hasattr(settings, "POSTGIS_VERSION"):
            version = settings.POSTGIS_VERSION
        else:
            
            
            
            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.0. "
                    "Was the database created from a spatial database "
                    "template?" % self.connection.settings_dict["NAME"]
                )
            version = vtup[1:]
        return version

    def convert_extent(self, box):
        
        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):
        
        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):
        
        if f.geom_type == "RASTER":
            return "raster"

        
        
        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):
        
        
        value = dist_val[0]

        
        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:
            
            dist_param = value

        return [dist_param]

    def get_geom_placeholder(self, f, value, compiler):
        
        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

        
        if value is None:
            value_srid = None
        else:
            value_srid = value.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):
        
        
        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 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):
        
        version = self.postgis_lib_version()
        return (version,) + get_version_tuple(version)

    def proj_version_tuple(self):
        
        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

    
    def geometry_columns(self):
        return PostGISGeometryColumns

    def spatial_ref_sys(self):
        return PostGISSpatialRefSys

    def parse_raster(self, value):
        
        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):  
                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"