Skip to content

Commit 0f3b1a7

Browse files
olibookfelixxm
authored andcommitted
Fixed #34739 -- Added GEOSGeometry.equals_identical() method.
1 parent 8edaf07 commit 0f3b1a7

File tree

6 files changed

+96
-2
lines changed

6 files changed

+96
-2
lines changed

django/contrib/gis/geos/geometry.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.contrib.gis.geos.base import GEOSBase
1212
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
1313
from django.contrib.gis.geos.error import GEOSException
14-
from django.contrib.gis.geos.libgeos import GEOM_PTR
14+
from django.contrib.gis.geos.libgeos import GEOM_PTR, geos_version_tuple
1515
from django.contrib.gis.geos.mutable_list import ListMixin
1616
from django.contrib.gis.geos.prepared import PreparedGeometry
1717
from django.contrib.gis.geos.prototypes.io import ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w
@@ -318,6 +318,16 @@ def equals_exact(self, other, tolerance=0):
318318
"""
319319
return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
320320

321+
def equals_identical(self, other):
322+
"""
323+
Return true if the two Geometries are point-wise equivalent.
324+
"""
325+
if geos_version_tuple() < (3, 12):
326+
raise GEOSException(
327+
"GEOSGeometry.equals_identical() requires GEOS >= 3.12.0."
328+
)
329+
return capi.geos_equalsidentical(self.ptr, other.ptr)
330+
321331
def intersects(self, other):
322332
"Return true if disjoint return false."
323333
return capi.geos_intersects(self.ptr, other.ptr)

django/contrib/gis/geos/prototypes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
geos_disjoint,
5252
geos_equals,
5353
geos_equalsexact,
54+
geos_equalsidentical,
5455
geos_hasz,
5556
geos_intersects,
5657
geos_isclosed,

django/contrib/gis/geos/prototypes/predicates.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BinaryPredicate(UnaryPredicate):
3838
geos_equalsexact = BinaryPredicate(
3939
"GEOSEqualsExact", argtypes=[GEOM_PTR, GEOM_PTR, c_double]
4040
)
41+
geos_equalsidentical = BinaryPredicate("GEOSEqualsIdentical")
4142
geos_intersects = BinaryPredicate("GEOSIntersects")
4243
geos_overlaps = BinaryPredicate("GEOSOverlaps")
4344
geos_relatepattern = BinaryPredicate(

docs/ref/contrib/gis/geos.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,15 @@ return a boolean.
483483
``poly1.equals_exact(poly2, 0.001)`` will compare equality to within
484484
one thousandth of a unit.
485485

486+
.. method:: GEOSGeometry.equals_identical(other)
487+
488+
.. versionadded:: 5.0
489+
490+
Returns ``True`` if the two geometries are point-wise equivalent by
491+
checking that the structure, ordering, and values of all vertices are
492+
identical in all dimensions. ``NaN`` values are considered to be equal to
493+
other ``NaN`` values. Requires GEOS 3.12.
494+
486495
.. method:: GEOSGeometry.intersects(other)
487496

488497
Returns ``True`` if :meth:`GEOSGeometry.disjoint` is ``False``.

docs/releases/5.0.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ Minor features
192192

193193
* Added support for GEOS 3.12.
194194

195+
* The new :meth:`.GEOSGeometry.equals_identical` method allows point-wise
196+
equivalence checking of geometries.
197+
195198
:mod:`django.contrib.messages`
196199
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
197200

tests/gis_tests/geos_tests/test_geos.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import ctypes
22
import itertools
33
import json
4+
import math
45
import pickle
56
import random
67
from binascii import a2b_hex
78
from io import BytesIO
8-
from unittest import mock
9+
from unittest import mock, skipIf
910

1011
from django.contrib.gis import gdal
1112
from django.contrib.gis.geos import (
@@ -241,6 +242,75 @@ def test_eq_with_srid(self):
241242
self.assertEqual(p0, "SRID=0;POINT (5 23)")
242243
self.assertNotEqual(p1, "SRID=0;POINT (5 23)")
243244

245+
@skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required")
246+
def test_equals_identical(self):
247+
tests = [
248+
# Empty inputs of different types are not equals_identical.
249+
("POINT EMPTY", "LINESTRING EMPTY", False),
250+
# Empty inputs of different dimensions are not equals_identical.
251+
("POINT EMPTY", "POINT Z EMPTY", False),
252+
# Non-empty inputs of different dimensions are not equals_identical.
253+
("POINT Z (1 2 3)", "POINT M (1 2 3)", False),
254+
("POINT ZM (1 2 3 4)", "POINT Z (1 2 3)", False),
255+
# Inputs with different structure are not equals_identical.
256+
("LINESTRING (1 1, 2 2)", "MULTILINESTRING ((1 1, 2 2))", False),
257+
# Inputs with different types are not equals_identical.
258+
(
259+
"GEOMETRYCOLLECTION (LINESTRING (1 1, 2 2))",
260+
"MULTILINESTRING ((1 1, 2 2))",
261+
False,
262+
),
263+
# Same lines are equals_identical.
264+
("LINESTRING M (1 1 0, 2 2 1)", "LINESTRING M (1 1 0, 2 2 1)", True),
265+
# Different lines are not equals_identical.
266+
("LINESTRING M (1 1 0, 2 2 1)", "LINESTRING M (1 1 1, 2 2 1)", False),
267+
# Same polygons are equals_identical.
268+
("POLYGON ((0 0, 1 0, 1 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 0))", True),
269+
# Different polygons are not equals_identical.
270+
("POLYGON ((0 0, 1 0, 1 1, 0 0))", "POLYGON ((1 0, 1 1, 0 0, 1 0))", False),
271+
# Different polygons (number of holes) are not equals_identical.
272+
(
273+
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1))",
274+
(
275+
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), "
276+
"(3 3, 4 3, 4 4, 3 3))"
277+
),
278+
False,
279+
),
280+
# Same collections are equals_identical.
281+
(
282+
"MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
283+
"MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
284+
True,
285+
),
286+
# Different collections (structure) are not equals_identical.
287+
(
288+
"MULTILINESTRING ((1 1, 2 2), (2 2, 3 3))",
289+
"MULTILINESTRING ((2 2, 3 3), (1 1, 2 2))",
290+
False,
291+
),
292+
]
293+
for g1, g2, is_equal_identical in tests:
294+
with self.subTest(g1=g1, g2=g2):
295+
self.assertIs(
296+
fromstr(g1).equals_identical(fromstr(g2)), is_equal_identical
297+
)
298+
299+
@skipIf(geos_version_tuple() < (3, 12), "GEOS >= 3.12.0 is required")
300+
def test_infinite_values_equals_identical(self):
301+
# Input with identical infinite values are equals_identical.
302+
g1 = Point(x=float("nan"), y=math.inf)
303+
g2 = Point(x=float("nan"), y=math.inf)
304+
self.assertIs(g1.equals_identical(g2), True)
305+
306+
@mock.patch("django.contrib.gis.geos.libgeos.geos_version", lambda: b"3.11.0")
307+
def test_equals_identical_geos_version(self):
308+
g1 = fromstr("POINT (1 2 3)")
309+
g2 = fromstr("POINT (1 2 3)")
310+
msg = "GEOSGeometry.equals_identical() requires GEOS >= 3.12.0"
311+
with self.assertRaisesMessage(GEOSException, msg):
312+
g1.equals_identical(g2)
313+
244314
def test_points(self):
245315
"Testing Point objects."
246316
prev = fromstr("POINT(0 0)")

0 commit comments

Comments
 (0)