Browse Source

Refs #36036 -- Added support for GEOSHasM.

Andrew Harris 2 months ago
parent
commit
5f30fd2358

+ 8 - 1
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."

+ 1 - 0
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,

+ 1 - 0
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")

+ 8 - 1
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
 

+ 2 - 1
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 14 - 5
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)")

+ 36 - 0
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