diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py
index dec3495d25c9..febeeebfa3bc 100644
--- a/django/contrib/gis/geos/coordseq.py
+++ b/django/contrib/gis/geos/coordseq.py
@@ -9,7 +9,7 @@
from django.contrib.gis.geos import prototypes as capi
from django.contrib.gis.geos.base import GEOSBase
from django.contrib.gis.geos.error import GEOSException
-from django.contrib.gis.geos.libgeos import CS_PTR
+from django.contrib.gis.geos.libgeos import CS_PTR, geos_version_tuple
from django.contrib.gis.shortcuts import numpy
@@ -20,6 +20,8 @@ class GEOSCoordSeq(GEOSBase):
def __init__(self, ptr, z=False):
"Initialize from a GEOS pointer."
+ # TODO when dropping support for GEOS 3.13 the z argument can be
+ # deprecated in favor of using the GEOS function GEOSCoordSeq_hasZ.
if not isinstance(ptr, CS_PTR):
raise TypeError("Coordinate sequence should initialize with a CS_PTR.")
self._ptr = ptr
@@ -58,6 +60,12 @@ def __setitem__(self, index, value):
if self.dims == 3 and self._z:
n_args = 3
point_setter = self._set_point_3d
+ elif self.dims == 3 and self.hasm:
+ n_args = 3
+ point_setter = self._set_point_3d_m
+ elif self.dims == 4 and self._z and self.hasm:
+ n_args = 4
+ point_setter = self._set_point_4d
else:
n_args = 2
point_setter = self._set_point_2d
@@ -74,7 +82,7 @@ def _checkindex(self, index):
def _checkdim(self, dim):
"Check the given dimension."
- if dim < 0 or dim > 2:
+ if dim < 0 or dim > 3:
raise GEOSException(f'Invalid ordinate dimension: "{dim:d}"')
def _get_x(self, index):
@@ -86,6 +94,9 @@ def _get_y(self, index):
def _get_z(self, index):
return capi.cs_getz(self.ptr, index, byref(c_double()))
+ def _get_m(self, index):
+ return capi.cs_getm(self.ptr, index, byref(c_double()))
+
def _set_x(self, index, value):
capi.cs_setx(self.ptr, index, value)
@@ -95,9 +106,18 @@ def _set_y(self, index, value):
def _set_z(self, index, value):
capi.cs_setz(self.ptr, index, value)
+ def _set_m(self, index, value):
+ capi.cs_setm(self.ptr, index, value)
+
@property
def _point_getter(self):
- return self._get_point_3d if self.dims == 3 and self._z else self._get_point_2d
+ if self.dims == 3 and self._z:
+ return self._get_point_3d
+ elif self.dims == 3 and self.hasm:
+ return self._get_point_3d_m
+ elif self.dims == 4 and self._z and self.hasm:
+ return self._get_point_4d
+ return self._get_point_2d
def _get_point_2d(self, index):
return (self._get_x(index), self._get_y(index))
@@ -105,6 +125,17 @@ def _get_point_2d(self, index):
def _get_point_3d(self, index):
return (self._get_x(index), self._get_y(index), self._get_z(index))
+ def _get_point_3d_m(self, index):
+ return (self._get_x(index), self._get_y(index), self._get_m(index))
+
+ def _get_point_4d(self, index):
+ return (
+ self._get_x(index),
+ self._get_y(index),
+ self._get_z(index),
+ self._get_m(index),
+ )
+
def _set_point_2d(self, index, value):
x, y = value
self._set_x(index, x)
@@ -116,6 +147,19 @@ def _set_point_3d(self, index, value):
self._set_y(index, y)
self._set_z(index, z)
+ def _set_point_3d_m(self, index, value):
+ x, y, m = value
+ self._set_x(index, x)
+ self._set_y(index, y)
+ self._set_m(index, m)
+
+ def _set_point_4d(self, index, value):
+ x, y, z, m = value
+ self._set_x(index, x)
+ self._set_y(index, y)
+ self._set_z(index, z)
+ self._set_m(index, m)
+
# #### Ordinate getting and setting routines ####
def getOrdinate(self, dimension, index):
"Return the value for the given dimension and index."
@@ -153,6 +197,14 @@ def setZ(self, index, value):
"Set Z with the value at the given index."
self.setOrdinate(2, index, value)
+ def getM(self, index):
+ "Get M with the value at the given index."
+ return self.getOrdinate(3, index)
+
+ def setM(self, index, value):
+ "Set M with the value at the given index."
+ self.setOrdinate(3, index, value)
+
# ### Dimensions ###
@property
def size(self):
@@ -172,6 +224,18 @@ def hasz(self):
"""
return self._z
+ @property
+ def hasm(self):
+ """
+ Return whether this coordinate sequence has M dimension.
+ """
+ if geos_version_tuple() >= (3, 14):
+ return capi.cs_hasm(self._ptr)
+ else:
+ raise NotImplementedError(
+ "GEOSCoordSeq with an M dimension requires GEOS 3.14+."
+ )
+
# ### Other Methods ###
def clone(self):
"Clone this coordinate sequence."
@@ -180,16 +244,13 @@ def clone(self):
@property
def kml(self):
"Return the KML representation for the coordinates."
- # Getting the substitution string depending on whether the coordinates
- # have a Z dimension.
if self.hasz:
- substr = "%s,%s,%s "
+ coords = [f"{coord[0]},{coord[1]},{coord[2]}" for coord in self]
else:
- substr = "%s,%s,0 "
- return (
- "%s"
- % "".join(substr % self[i] for i in range(len(self))).strip()
- )
+ coords = [f"{coord[0]},{coord[1]},0" for coord in self]
+
+ coordinate_string = " ".join(coords)
+ return f"{coordinate_string}"
@property
def tuple(self):
diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py
index 6b0da37ee676..cac0b3fdf46b 100644
--- a/django/contrib/gis/geos/prototypes/__init__.py
+++ b/django/contrib/gis/geos/prototypes/__init__.py
@@ -8,12 +8,15 @@
create_cs,
cs_clone,
cs_getdims,
+ cs_getm,
cs_getordinate,
cs_getsize,
cs_getx,
cs_gety,
cs_getz,
+ cs_hasm,
cs_is_ccw,
+ cs_setm,
cs_setordinate,
cs_setx,
cs_sety,
diff --git a/django/contrib/gis/geos/prototypes/coordseq.py b/django/contrib/gis/geos/prototypes/coordseq.py
index cfc242c00dd1..eadaf8dfcf1e 100644
--- a/django/contrib/gis/geos/prototypes/coordseq.py
+++ b/django/contrib/gis/geos/prototypes/coordseq.py
@@ -1,7 +1,15 @@
from ctypes import POINTER, c_byte, c_double, c_int, c_uint
-from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory
-from django.contrib.gis.geos.prototypes.errcheck import GEOSException, last_arg_byref
+from django.contrib.gis.geos.libgeos import (
+ CS_PTR,
+ GEOM_PTR,
+ GEOSFuncFactory,
+)
+from django.contrib.gis.geos.prototypes.errcheck import (
+ GEOSException,
+ check_predicate,
+ last_arg_byref,
+)
# ## Error-checking routines specific to coordinate sequences. ##
@@ -67,6 +75,12 @@ def errcheck(result, func, cargs):
return result
+class CsUnaryPredicate(GEOSFuncFactory):
+ argtypes = [CS_PTR]
+ restype = c_byte
+ errcheck = staticmethod(check_predicate)
+
+
# ## Coordinate Sequence ctypes prototypes ##
# Coordinate Sequence constructors & cloning.
@@ -78,20 +92,25 @@ def errcheck(result, func, cargs):
cs_getordinate = CsOperation("GEOSCoordSeq_getOrdinate", ordinate=True, get=True)
cs_setordinate = CsOperation("GEOSCoordSeq_setOrdinate", ordinate=True)
-# For getting, x, y, z
+# For getting, x, y, z, m
cs_getx = CsOperation("GEOSCoordSeq_getX", get=True)
cs_gety = CsOperation("GEOSCoordSeq_getY", get=True)
cs_getz = CsOperation("GEOSCoordSeq_getZ", get=True)
+cs_getm = CsOperation("GEOSCoordSeq_getM", get=True)
-# For setting, x, y, z
+# For setting, x, y, z, m
cs_setx = CsOperation("GEOSCoordSeq_setX")
cs_sety = CsOperation("GEOSCoordSeq_setY")
cs_setz = CsOperation("GEOSCoordSeq_setZ")
+cs_setm = CsOperation("GEOSCoordSeq_setM")
# These routines return size & dimensions.
cs_getsize = CsInt("GEOSCoordSeq_getSize")
cs_getdims = CsInt("GEOSCoordSeq_getDimensions")
+# Unary Predicates
+cs_hasm = CsUnaryPredicate("GEOSCoordSeq_hasM")
+
cs_is_ccw = GEOSFuncFactory(
"GEOSCoordSeq_isCCW", restype=c_int, argtypes=[CS_PTR, POINTER(c_byte)]
)
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index bad4a79d6164..6a32a0728e04 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -25,7 +25,6 @@ plain text message::
"Here is the message.",
"from@example.com",
["to@example.com"],
- fail_silently=False,
)
When additional email sending functionality is needed, use
diff --git a/tests/gis_tests/geos_tests/test_coordseq.py b/tests/gis_tests/geos_tests/test_coordseq.py
index b6f5136dd19c..37c421f5c3b8 100644
--- a/tests/gis_tests/geos_tests/test_coordseq.py
+++ b/tests/gis_tests/geos_tests/test_coordseq.py
@@ -1,4 +1,11 @@
-from django.contrib.gis.geos import LineString
+import math
+from unittest import skipIf
+from unittest.mock import patch
+
+from django.contrib.gis.geos import GEOSGeometry, LineString
+from django.contrib.gis.geos import prototypes as capi
+from django.contrib.gis.geos.coordseq import GEOSCoordSeq
+from django.contrib.gis.geos.libgeos import geos_version_tuple
from django.test import SimpleTestCase
@@ -13,3 +20,130 @@ def test_getitem(self):
with self.subTest(i):
with self.assertRaisesMessage(IndexError, msg):
coord_seq[i]
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_has_m(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertIs(coord_seq.hasm, True)
+
+ geom = GEOSGeometry("POINT Z (1 2 3)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertIs(coord_seq.hasm, False)
+
+ geom = GEOSGeometry("POINT M (1 2 3)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertIs(coord_seq.hasm, True)
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_get_set_m(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertEqual(coord_seq.tuple, (1, 2, 3, 4))
+ self.assertEqual(coord_seq.getM(0), 4)
+ coord_seq.setM(0, 10)
+ self.assertEqual(coord_seq.tuple, (1, 2, 3, 10))
+ self.assertEqual(coord_seq.getM(0), 10)
+
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.tuple, (1, 2, 4))
+ self.assertEqual(coord_seq.getM(0), 4)
+ coord_seq.setM(0, 10)
+ self.assertEqual(coord_seq.tuple, (1, 2, 10))
+ self.assertEqual(coord_seq.getM(0), 10)
+ self.assertIs(math.isnan(coord_seq.getZ(0)), True)
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_setitem(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ coord_seq[0] = (10, 20, 30, 40)
+ self.assertEqual(coord_seq.tuple, (10, 20, 30, 40))
+
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ coord_seq[0] = (10, 20, 40)
+ self.assertEqual(coord_seq.tuple, (10, 20, 40))
+ self.assertEqual(coord_seq.getM(0), 40)
+ self.assertIs(math.isnan(coord_seq.getZ(0)), True)
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_kml_m_dimension(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertEqual(coord_seq.kml, "1.0,2.0,3.0")
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.kml, "1.0,2.0,0")
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_clone_m_dimension(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ clone = coord_seq.clone()
+ self.assertEqual(clone.tuple, (1, 2, 3, 4))
+ self.assertIs(clone.hasz, True)
+ self.assertIs(clone.hasm, True)
+
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ clone = coord_seq.clone()
+ self.assertEqual(clone.tuple, (1, 2, 4))
+ self.assertIs(clone.hasz, False)
+ self.assertIs(clone.hasm, True)
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_dims(self):
+ geom = GEOSGeometry("POINT ZM (1 2 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertEqual(coord_seq.dims, 4)
+
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.dims, 3)
+
+ geom = GEOSGeometry("POINT Z (1 2 3)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertEqual(coord_seq.dims, 3)
+
+ geom = GEOSGeometry("POINT (1 2)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.dims, 2)
+
+ def test_size(self):
+ geom = GEOSGeometry("POINT (1 2)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.size, 1)
+
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
+ self.assertEqual(coord_seq.size, 1)
+
+ @skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
+ def test_iscounterclockwise(self):
+ geom = GEOSGeometry("LINEARRING ZM (0 0 3 0, 1 0 0 2, 0 1 1 3, 0 0 3 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ self.assertEqual(
+ coord_seq.tuple,
+ (
+ (0.0, 0.0, 3.0, 0.0),
+ (1.0, 0.0, 0.0, 2.0),
+ (0.0, 1.0, 1.0, 3.0),
+ (0.0, 0.0, 3.0, 4.0),
+ ),
+ )
+ self.assertIs(coord_seq.is_counterclockwise, True)
+
+ def test_m_support_error(self):
+ geom = GEOSGeometry("POINT M (1 2 4)")
+ coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
+ msg = "GEOSCoordSeq with an M dimension requires GEOS 3.14+."
+
+ # mock geos_version_tuple to be 3.13.13
+ with patch(
+ "django.contrib.gis.geos.coordseq.geos_version_tuple",
+ return_value=(3, 13, 13),
+ ):
+ with self.assertRaisesMessage(NotImplementedError, msg):
+ coord_seq.hasm