From 94d86713a4ad0a87d3f4bcf75568cb04aeb6a6f3 Mon Sep 17 00:00:00 2001 From: leondaz Date: Sat, 12 Oct 2024 07:39:30 +0300 Subject: [PATCH] added support for geom_type lookup --- django/contrib/gis/db/models/functions.py | 32 +++++++++++ docs/ref/contrib/gis/geoquerysets.txt | 28 +++++++++ docs/releases/5.2.txt | 1 + tests/gis_tests/geoapp/test_functions.py | 70 +++++++++++++++++++++-- 4 files changed, 127 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index 10272b7d68..c9ab7fa9ae 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -8,6 +8,7 @@ from django.db import NotSupportedError from django.db.models import ( BinaryField, BooleanField, + CharField, FloatField, Func, IntegerField, @@ -421,6 +422,37 @@ class Intersection(OracleToleranceMixin, GeomOutputGeoFunc): geom_param_pos = (0, 1) +@BaseSpatialField.register_lookup +class GeometryType(GeoFuncMixin, Transform): + function = "GeometryType" + output_field = CharField() + lookup_name = "geom_type" + + def as_mysql(self, compiler, connection, **extra_context): + lhs, params = compiler.compile(self.lhs) + sql = f"ST_GeometryType({lhs})" + return sql, params + + def as_oracle(self, compiler, connection, **extra_context): + lhs, params = compiler.compile(self.lhs) + + sql = f""" + (SELECT DECODE( + SDO_GEOMETRY.GET_GTYPE({lhs}), + 1, 'POINT', + 2, 'LINESTRING', + 3, 'POLYGON', + 4, 'COLLECTION', + 5, 'MULTIPOINT', + 6, 'MULTILINESTRING', + 7, 'MULTIPOLYGON', + 8, 'SOLID', + 'UNKNOWN') + ) + """ + return sql, params + + @BaseSpatialField.register_lookup class IsEmpty(GeoFuncMixin, Transform): lookup_name = "isempty" diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index b639c5271e..fc59baca5e 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -692,6 +692,34 @@ PostGIS equivalent: .. _distance-lookups: +.. fieldlookup:: geom_type + +``geom_type`` +----------------- + +*Availability*: `PostGIS `__, +Oracle, MariaDB, MySQL, SpatiaLite + +Returns the geometry type of the geometry field. + +Example:: + + Shape.objects.filter(poly__geom_type=GeometryType("circle")) + +========== ========================== +Backend SQL Equivalent +========== ========================== +PostGIS ``GeometryType(geom)`` +MariaDB ``ST_GeometryType(geom)`` +MySQL ``ST_GeometryType(geom)`` +Oracle ``SDO_GEOMETRY.GET_GTYPE(geom)`` +SpatiaLite ``GeometryType(geom)`` +========== ========================== + +Note that the ``GeometryType`` functions returns the string type but +``SDO_GEOMETRY.GET_GTYPE`` function returns the type encoded as a number. +Thus, those numbers are mapped to the string counterparts as per `Oracle's documentation `_ + Distance Lookups ================ diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 507676d998..9a21a0ebb3 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -99,6 +99,7 @@ Minor features :attr:`.OGRGeometry.has_curve` property, and the :meth:`.OGRGeometry.get_linear_geometry` and :meth:`.OGRGeometry.get_curve_geometry` methods. +* Introduced :lookup:`geom_type` lookup to allow filtering by geometry type (:ticket:`28696`) :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 80b08f8d39..6387f182fd 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -4,14 +4,31 @@ import re from decimal import Decimal from django.contrib.gis.db.models import GeometryField, PolygonField, functions -from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon, fromstr +from django.contrib.gis.geos import ( + GEOSGeometry, + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, + fromstr, +) from django.contrib.gis.measure import Area from django.db import NotSupportedError, connection -from django.db.models import IntegerField, Sum, Value -from django.test import TestCase, skipUnlessDBFeature +from django.db.models import F, IntegerField, Sum, Value +from django.test import TestCase, skipUnlessAnyDBFeature, skipUnlessDBFeature from ..utils import FuncTestMixin -from .models import City, Country, CountryWebMercator, ManyPointModel, State, Track +from .models import ( + City, + Country, + CountryWebMercator, + Feature, + ManyPointModel, + State, + Track, +) class GISFunctionsTests(FuncTestMixin, TestCase): @@ -845,3 +862,48 @@ class GISFunctionsTests(FuncTestMixin, TestCase): City.objects.annotate(union=functions.GeoFunc(1, "point")).get( name="Dallas" ) + + +class GeometryTypeFunctionTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.features = [ + Feature.objects.create(name="Point", geom=Point(0, 0)), + Feature.objects.create(name="LineString", geom=LineString((0, 0), (1, 1))), + Feature.objects.create( + name="Polygon", geom=Polygon(((0, 0), (1, 0), (1, 1), (0, 0))) + ), + Feature.objects.create( + name="MultiPoint", geom=MultiPoint(Point(0, 0), Point(1, 1)) + ), + Feature.objects.create( + name="MultiLineString", + geom=MultiLineString( + LineString((0, 0), (1, 1)), LineString((1, 1), (2, 2)) + ), + ), + Feature.objects.create( + name="MultiPolygon", + geom=MultiPolygon( + Polygon(((0, 0), (1, 0), (1, 1), (0, 0))), + Polygon(((1, 1), (2, 1), (2, 2), (1, 1))), + ), + ), + ] + + @skipUnlessAnyDBFeature("has_GeometryType_function", "has_SDO_GTYPE_function") + def test_geometry_type_transform(self): + qs = Feature.objects.annotate(geom_type=F("geom__geom_type")) + + expected_results = { + "Point": "POINT", + "LineString": "LINESTRING", + "Polygon": "POLYGON", + "MultiPoint": "MULTIPOINT", + "MultiLineString": "MULTILINESTRING", + "MultiPolygon": "MULTIPOLYGON", + } + + for feature in qs: + expected_type = expected_results[feature.name] + self.assertEqual(feature.geom_type.upper(), expected_type)