浏览代码

Fixed #34739 -- Added GEOSGeometry.equals_identical() method.

Olivier Tabone 1 年之前
父节点
当前提交
0f3b1a783d

+ 11 - 1
django/contrib/gis/geos/geometry.py

@@ -11,7 +11,7 @@ from django.contrib.gis.geos import prototypes as capi
 from django.contrib.gis.geos.base import GEOSBase
 from django.contrib.gis.geos.coordseq import GEOSCoordSeq
 from django.contrib.gis.geos.error import GEOSException
-from django.contrib.gis.geos.libgeos import GEOM_PTR
+from django.contrib.gis.geos.libgeos import GEOM_PTR, geos_version_tuple
 from django.contrib.gis.geos.mutable_list import ListMixin
 from django.contrib.gis.geos.prepared import PreparedGeometry
 from django.contrib.gis.geos.prototypes.io import ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w
@@ -318,6 +318,16 @@ class GEOSGeometryBase(GEOSBase):
         """
         return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
 
+    def equals_identical(self, other):
+        """
+        Return true if the two Geometries are point-wise equivalent.
+        """
+        if geos_version_tuple() < (3, 12):
+            raise GEOSException(
+                "GEOSGeometry.equals_identical() requires GEOS >= 3.12.0."
+            )
+        return capi.geos_equalsidentical(self.ptr, other.ptr)
+
     def intersects(self, other):
         "Return true if disjoint return false."
         return capi.geos_intersects(self.ptr, other.ptr)

+ 1 - 0
django/contrib/gis/geos/prototypes/__init__.py

@@ -51,6 +51,7 @@ from django.contrib.gis.geos.prototypes.predicates import (  # NOQA
     geos_disjoint,
     geos_equals,
     geos_equalsexact,
+    geos_equalsidentical,
     geos_hasz,
     geos_intersects,
     geos_isclosed,

+ 1 - 0
django/contrib/gis/geos/prototypes/predicates.py

@@ -38,6 +38,7 @@ geos_equals = BinaryPredicate("GEOSEquals")
 geos_equalsexact = BinaryPredicate(
     "GEOSEqualsExact", argtypes=[GEOM_PTR, GEOM_PTR, c_double]
 )
+geos_equalsidentical = BinaryPredicate("GEOSEqualsIdentical")
 geos_intersects = BinaryPredicate("GEOSIntersects")
 geos_overlaps = BinaryPredicate("GEOSOverlaps")
 geos_relatepattern = BinaryPredicate(

+ 9 - 0
docs/ref/contrib/gis/geos.txt

@@ -483,6 +483,15 @@ return a boolean.
     ``poly1.equals_exact(poly2, 0.001)`` will compare equality to within
     one thousandth of a unit.
 
+.. method:: GEOSGeometry.equals_identical(other)
+
+    .. versionadded:: 5.0
+
+    Returns ``True`` if the two geometries are point-wise equivalent by
+    checking that the structure, ordering, and values of all vertices are
+    identical in all dimensions. ``NaN`` values are considered to be equal to
+    other ``NaN`` values. Requires GEOS 3.12.
+
 .. method:: GEOSGeometry.intersects(other)
 
     Returns ``True`` if :meth:`GEOSGeometry.disjoint` is ``False``.

+ 3 - 0
docs/releases/5.0.txt

@@ -192,6 +192,9 @@ Minor features
 
 * Added support for GEOS 3.12.
 
+* The new :meth:`.GEOSGeometry.equals_identical` method allows point-wise
+  equivalence checking of geometries.
+
 :mod:`django.contrib.messages`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 71 - 1
tests/gis_tests/geos_tests/test_geos.py

@@ -1,11 +1,12 @@
 import ctypes
 import itertools
 import json
+import math
 import pickle
 import random
 from binascii import a2b_hex
 from io import BytesIO
-from unittest import mock
+from unittest import mock, skipIf
 
 from django.contrib.gis import gdal
 from django.contrib.gis.geos import (
@@ -241,6 +242,75 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
         self.assertEqual(p0, "SRID=0;POINT (5 23)")
         self.assertNotEqual(p1, "SRID=0;POINT (5 23)")
 
+    @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required")
+    def test_equals_identical(self):
+        tests = [
+            # Empty inputs of different types are not equals_identical.
+            ("POINT EMPTY", "LINESTRING EMPTY", False),
+            # Empty inputs of different dimensions are not equals_identical.
+            ("POINT EMPTY", "POINT Z EMPTY", False),
+            # Non-empty inputs of different dimensions are not equals_identical.
+            ("POINT Z (1 2 3)", "POINT M (1 2 3)", False),
+            ("POINT ZM (1 2 3 4)", "POINT Z (1 2 3)", False),
+            # Inputs with different structure are not equals_identical.
+            ("LINESTRING (1 1, 2 2)", "MULTILINESTRING ((1 1, 2 2))", False),
+            # Inputs with different types are not equals_identical.
+            (
+                "GEOMETRYCOLLECTION (LINESTRING (1 1, 2 2))",
+                "MULTILINESTRING ((1 1, 2 2))",
+                False,
+            ),
+            # Same lines are equals_identical.
+            ("LINESTRING M (1 1 0, 2 2 1)", "LINESTRING M (1 1 0, 2 2 1)", True),
+            # Different lines are not equals_identical.
+            ("LINESTRING M (1 1 0, 2 2 1)", "LINESTRING M (1 1 1, 2 2 1)", False),
+            # Same polygons are equals_identical.
+            ("POLYGON ((0 0, 1 0, 1 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 0))", True),
+            # Different polygons are not equals_identical.
+            ("POLYGON ((0 0, 1 0, 1 1, 0 0))", "POLYGON ((1 0, 1 1, 0 0, 1 0))", False),
+            # Different polygons (number of holes) are not equals_identical.
+            (
+                "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1))",
+                (
+                    "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), "
+                    "(3 3, 4 3, 4 4, 3 3))"
+                ),
+                False,
+            ),
+            # Same collections are equals_identical.
+            (
+                "MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
+                "MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
+                True,
+            ),
+            # Different collections (structure) are not equals_identical.
+            (
+                "MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
+                "MULTILINESTRING ((2 2, 3 3), (1 1, 2 2))",
+                False,
+            ),
+        ]
+        for g1, g2, is_equal_identical in tests:
+            with self.subTest(g1=g1, g2=g2):
+                self.assertIs(
+                    fromstr(g1).equals_identical(fromstr(g2)), is_equal_identical
+                )
+
+    @skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required")
+    def test_infinite_values_equals_identical(self):
+        # Input with identical infinite values are equals_identical.
+        g1 = Point(x=float("nan"), y=math.inf)
+        g2 = Point(x=float("nan"), y=math.inf)
+        self.assertIs(g1.equals_identical(g2), True)
+
+    @mock.patch("django.contrib.gis.geos.libgeos.geos_version", lambda: b"3.11.0")
+    def test_equals_identical_geos_version(self):
+        g1 = fromstr("POINT (1 2 3)")
+        g2 = fromstr("POINT (1 2 3)")
+        msg = "GEOSGeometry.equals_identical() requires GEOS >= 3.12.0"
+        with self.assertRaisesMessage(GEOSException, msg):
+            g1.equals_identical(g2)
+
     def test_points(self):
         "Testing Point objects."
         prev = fromstr("POINT(0 0)")