From 5f30fd2358fd60a514bdba31594bfc8122f30167 Mon Sep 17 00:00:00 2001 From: Andrew Harris Date: Fri, 10 Jan 2025 10:11:03 -0500 Subject: [PATCH] Refs #36036 -- Added support for GEOSHasM. --- django/contrib/gis/geos/geometry.py | 9 ++++- .../contrib/gis/geos/prototypes/__init__.py | 1 + .../contrib/gis/geos/prototypes/predicates.py | 1 + docs/ref/contrib/gis/geos.txt | 9 ++++- docs/releases/6.0.txt | 3 +- tests/gis_tests/gdal_tests/test_geom.py | 19 +++++++--- tests/gis_tests/geos_tests/test_geos.py | 36 +++++++++++++++++++ 7 files changed, 70 insertions(+), 8 deletions(-) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 8bbe2c264a..c8c451d99f 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -252,9 +252,16 @@ class GEOSGeometryBase(GEOSBase): @property def hasz(self): - "Return whether the geometry has a 3D dimension." + "Return whether the geometry has a Z dimension." return capi.geos_hasz(self.ptr) + @property + def hasm(self): + "Return whether the geometry has a M dimension." + if geos_version_tuple() < (3, 12): + raise GEOSException("GEOSGeometry.hasm requires GEOS >= 3.12.0.") + return capi.geos_hasm(self.ptr) + @property def ring(self): "Return whether or not the geometry is a ring." diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py index 3e980b9b89..94dcee31f1 100644 --- a/django/contrib/gis/geos/prototypes/__init__.py +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -52,6 +52,7 @@ from django.contrib.gis.geos.prototypes.predicates import ( # NOQA geos_equals, geos_equalsexact, geos_equalsidentical, + geos_hasm, geos_hasz, geos_intersects, geos_isclosed, diff --git a/django/contrib/gis/geos/prototypes/predicates.py b/django/contrib/gis/geos/prototypes/predicates.py index dd07cbfb26..e5c2298d4e 100644 --- a/django/contrib/gis/geos/prototypes/predicates.py +++ b/django/contrib/gis/geos/prototypes/predicates.py @@ -24,6 +24,7 @@ class BinaryPredicate(UnaryPredicate): # ## Unary Predicates ## geos_hasz = UnaryPredicate("GEOSHasZ") +geos_hasm = UnaryPredicate("GEOSHasM") geos_isclosed = UnaryPredicate("GEOSisClosed") geos_isempty = UnaryPredicate("GEOSisEmpty") geos_isring = UnaryPredicate("GEOSisRing") diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index b3825e09c4..58097434e8 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -312,7 +312,14 @@ Properties .. attribute:: GEOSGeometry.hasz - Returns a boolean indicating whether the geometry is three-dimensional. + Returns a boolean indicating whether the geometry has the Z dimension. + +.. attribute:: GEOSGeometry.hasm + + .. versionadded:: 6.0 + + Returns a boolean indicating whether the geometry has the M dimension. + Requires GEOS 3.12. .. attribute:: GEOSGeometry.ring diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 03b1c52c1d..9dd7100c74 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -65,7 +65,8 @@ Minor features :mod:`django.contrib.gis` ~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The new :attr:`.GEOSGeometry.hasm` property checks whether the geometry has + the M dimension. :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/gdal_tests/test_geom.py b/tests/gis_tests/gdal_tests/test_geom.py index 919e547511..a296af75f9 100644 --- a/tests/gis_tests/gdal_tests/test_geom.py +++ b/tests/gis_tests/gdal_tests/test_geom.py @@ -1,5 +1,6 @@ import json import pickle +from unittest import mock, skipIf from django.contrib.gis.gdal import ( CoordTransform, @@ -10,6 +11,7 @@ from django.contrib.gis.gdal import ( ) from django.contrib.gis.gdal.geometries import CircularString, CurvePolygon from django.contrib.gis.geos import GEOSException +from django.contrib.gis.geos.libgeos import geos_version_tuple from django.template import Context from django.template.engine import Engine from django.test import SimpleTestCase @@ -871,12 +873,19 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin): self.assertEqual(geom.geom_type.name, "PointM") self.assertEqual(geom.geom_type.num, 2001) + @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required") def test_point_m_dimension_geos(self): - """GEOSGeometry does not yet support the M dimension.""" - geom = OGRGeometry("POINT ZM (1 2 3 4)") - self.assertEqual(geom.geos.wkt, "POINT Z (1 2 3)") - geom = OGRGeometry("POINT M (1 2 3)") - self.assertEqual(geom.geos.wkt, "POINT (1 2)") + geo_zm = OGRGeometry("POINT ZM (1 2 3 4)") + self.assertEqual(geo_zm.geos.wkt, "POINT ZM (1 2 3 4)") + geo_m = OGRGeometry("POINT M (1 2 3)") + self.assertEqual(geo_m.geos.wkt, "POINT M (1 2 3)") + + @mock.patch("django.contrib.gis.geos.libgeos.geos_version", lambda: b"3.11.0") + def test_point_m_dimension_geos_version(self): + geo_zm = OGRGeometry("POINT ZM (1 2 3 4)") + self.assertEqual(geo_zm.geos.wkt, "POINT Z (1 2 3)") + geo_m = OGRGeometry("POINT M (1 2 3)") + self.assertEqual(geo_m.geos.wkt, "POINT (1 2)") def test_centroid(self): point = OGRGeometry("POINT (1 2 3)") diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index 0051a88b07..018eccae4a 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -86,6 +86,22 @@ class GEOSTest(SimpleTestCase, TestDataMixin): # Redundant sanity check. self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) + @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required") + def test_4d_hexewkb(self): + ogc_hex_4d = ( + b"01010000C00000000000000000000000000000" + b"F03F00000000000000400000000000000000" + ) + hexewkb_4d = ( + b"01010000E0E61000000000000000000000000000000000" + b"F03F00000000000000400000000000000000" + ) + pnt_4d = Point(0, 1, 2, 0, srid=4326) + self.assertEqual(ogc_hex_4d, pnt_4d.hex) + self.assertEqual(hexewkb_4d, pnt_4d.hexewkb) + self.assertIs(GEOSGeometry(hexewkb_4d).hasm, True) + self.assertEqual(memoryview(a2b_hex(hexewkb_4d)), pnt_4d.ewkb) + def test_kml(self): "Testing KML output." for tg in self.geometries.wkt_out: @@ -311,6 +327,20 @@ class GEOSTest(SimpleTestCase, TestDataMixin): with self.assertRaisesMessage(GEOSException, msg): g1.equals_identical(g2) + @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required") + def test_hasm(self): + pnt_xym = fromstr("POINT M (5 23 8)") + self.assertTrue(pnt_xym.hasm) + pnt_xyzm = fromstr("POINT (5 23 8 0)") + self.assertTrue(pnt_xyzm.hasm) + + @mock.patch("django.contrib.gis.geos.libgeos.geos_version", lambda: b"3.11.0") + def test_hasm_geos_version(self): + p = fromstr("POINT (1 2 3)") + msg = "GEOSGeometry.hasm requires GEOS >= 3.12.0." + with self.assertRaisesMessage(GEOSException, msg): + p.hasm + def test_points(self): "Testing Point objects." prev = fromstr("POINT(0 0)") @@ -1255,6 +1285,12 @@ class GEOSTest(SimpleTestCase, TestDataMixin): self.assertEqual(g2.hex, g2.ogr.hex) self.assertEqual("WGS 84", g2.srs.name) + @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required") + def test_gdal_4d(self): + g1_4d = fromstr("POINT(5 23 8 0)") + self.assertIsInstance(g1_4d.ogr, gdal.OGRGeometry) + self.assertEqual(g1_4d.ogr.m, 0) + def test_copy(self): "Testing use with the Python `copy` module." import copy