Skip to content

Commit 218077d

Browse files
smithdc1jacobtylerwalls
authored andcommitted
Refs #36036 -- Added m dimension to GEOSCoordSeq.
1 parent 7c54fee commit 218077d

4 files changed

Lines changed: 231 additions & 16 deletions

File tree

django/contrib/gis/geos/coordseq.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.contrib.gis.geos import prototypes as capi
1010
from django.contrib.gis.geos.base import GEOSBase
1111
from django.contrib.gis.geos.error import GEOSException
12-
from django.contrib.gis.geos.libgeos import CS_PTR
12+
from django.contrib.gis.geos.libgeos import CS_PTR, geos_version_tuple
1313
from django.contrib.gis.shortcuts import numpy
1414

1515

@@ -58,6 +58,12 @@ def __setitem__(self, index, value):
5858
if self.dims == 3 and self._z:
5959
n_args = 3
6060
point_setter = self._set_point_3d
61+
elif self.dims == 3 and self.hasm:
62+
n_args = 3
63+
point_setter = self._set_point_3d_m
64+
elif self.dims == 4 and self._z and self.hasm:
65+
n_args = 4
66+
point_setter = self._set_point_4d
6167
else:
6268
n_args = 2
6369
point_setter = self._set_point_2d
@@ -74,7 +80,7 @@ def _checkindex(self, index):
7480

7581
def _checkdim(self, dim):
7682
"Check the given dimension."
77-
if dim < 0 or dim > 2:
83+
if dim < 0 or dim > 3:
7884
raise GEOSException(f'Invalid ordinate dimension: "{dim:d}"')
7985

8086
def _get_x(self, index):
@@ -86,6 +92,9 @@ def _get_y(self, index):
8692
def _get_z(self, index):
8793
return capi.cs_getz(self.ptr, index, byref(c_double()))
8894

95+
def _get_m(self, index):
96+
return capi.cs_getm(self.ptr, index, byref(c_double()))
97+
8998
def _set_x(self, index, value):
9099
capi.cs_setx(self.ptr, index, value)
91100

@@ -95,16 +104,36 @@ def _set_y(self, index, value):
95104
def _set_z(self, index, value):
96105
capi.cs_setz(self.ptr, index, value)
97106

107+
def _set_m(self, index, value):
108+
capi.cs_setm(self.ptr, index, value)
109+
98110
@property
99111
def _point_getter(self):
100-
return self._get_point_3d if self.dims == 3 and self._z else self._get_point_2d
112+
if self.dims == 3 and self._z:
113+
return self._get_point_3d
114+
elif self.dims == 3 and self.hasm:
115+
return self._get_point_3d_m
116+
elif self.dims == 4 and self._z and self.hasm:
117+
return self._get_point_4d
118+
return self._get_point_2d
101119

102120
def _get_point_2d(self, index):
103121
return (self._get_x(index), self._get_y(index))
104122

105123
def _get_point_3d(self, index):
106124
return (self._get_x(index), self._get_y(index), self._get_z(index))
107125

126+
def _get_point_3d_m(self, index):
127+
return (self._get_x(index), self._get_y(index), self._get_m(index))
128+
129+
def _get_point_4d(self, index):
130+
return (
131+
self._get_x(index),
132+
self._get_y(index),
133+
self._get_z(index),
134+
self._get_m(index),
135+
)
136+
108137
def _set_point_2d(self, index, value):
109138
x, y = value
110139
self._set_x(index, x)
@@ -116,6 +145,19 @@ def _set_point_3d(self, index, value):
116145
self._set_y(index, y)
117146
self._set_z(index, z)
118147

148+
def _set_point_3d_m(self, index, value):
149+
x, y, m = value
150+
self._set_x(index, x)
151+
self._set_y(index, y)
152+
self._set_m(index, m)
153+
154+
def _set_point_4d(self, index, value):
155+
x, y, z, m = value
156+
self._set_x(index, x)
157+
self._set_y(index, y)
158+
self._set_z(index, z)
159+
self._set_m(index, m)
160+
119161
# #### Ordinate getting and setting routines ####
120162
def getOrdinate(self, dimension, index):
121163
"Return the value for the given dimension and index."
@@ -153,6 +195,14 @@ def setZ(self, index, value):
153195
"Set Z with the value at the given index."
154196
self.setOrdinate(2, index, value)
155197

198+
def getM(self, index):
199+
"Get M with the value at the given index."
200+
return self.getOrdinate(3, index)
201+
202+
def setM(self, index, value):
203+
"Set M with the value at the given index."
204+
self.setOrdinate(3, index, value)
205+
156206
# ### Dimensions ###
157207
@property
158208
def size(self):
@@ -172,6 +222,18 @@ def hasz(self):
172222
"""
173223
return self._z
174224

225+
@property
226+
def hasm(self):
227+
"""
228+
Return whether this coordinate sequence has M dimension.
229+
"""
230+
if geos_version_tuple() >= (3, 14):
231+
return capi.cs_hasm(self._ptr)
232+
else:
233+
raise NotImplementedError(
234+
"GEOSCoordSeq with an M dimension requires GEOS 3.14+."
235+
)
236+
175237
# ### Other Methods ###
176238
def clone(self):
177239
"Clone this coordinate sequence."
@@ -180,16 +242,13 @@ def clone(self):
180242
@property
181243
def kml(self):
182244
"Return the KML representation for the coordinates."
183-
# Getting the substitution string depending on whether the coordinates
184-
# have a Z dimension.
185245
if self.hasz:
186-
substr = "%s,%s,%s "
246+
coords = [f"{coord[0]},{coord[1]},{coord[2]}" for coord in self]
187247
else:
188-
substr = "%s,%s,0 "
189-
return (
190-
"<coordinates>%s</coordinates>"
191-
% "".join(substr % self[i] for i in range(len(self))).strip()
192-
)
248+
coords = [f"{coord[0]},{coord[1]},0" for coord in self]
249+
250+
coordinate_string = " ".join(coords)
251+
return f"<coordinates>{coordinate_string}</coordinates>"
193252

194253
@property
195254
def tuple(self):

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
create_cs,
99
cs_clone,
1010
cs_getdims,
11+
cs_getm,
1112
cs_getordinate,
1213
cs_getsize,
1314
cs_getx,
1415
cs_gety,
1516
cs_getz,
17+
cs_hasm,
1618
cs_is_ccw,
19+
cs_setm,
1720
cs_setordinate,
1821
cs_setx,
1922
cs_sety,

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from ctypes import POINTER, c_byte, c_double, c_int, c_uint
22

3-
from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory
4-
from django.contrib.gis.geos.prototypes.errcheck import GEOSException, last_arg_byref
3+
from django.contrib.gis.geos.libgeos import (
4+
CS_PTR,
5+
GEOM_PTR,
6+
GEOSFuncFactory,
7+
)
8+
from django.contrib.gis.geos.prototypes.errcheck import (
9+
GEOSException,
10+
check_predicate,
11+
last_arg_byref,
12+
)
513

614

715
# ## Error-checking routines specific to coordinate sequences. ##
@@ -67,6 +75,12 @@ def errcheck(result, func, cargs):
6775
return result
6876

6977

78+
class CsUnaryPredicate(GEOSFuncFactory):
79+
argtypes = [CS_PTR]
80+
restype = c_byte
81+
errcheck = staticmethod(check_predicate)
82+
83+
7084
# ## Coordinate Sequence ctypes prototypes ##
7185

7286
# Coordinate Sequence constructors & cloning.
@@ -78,20 +92,25 @@ def errcheck(result, func, cargs):
7892
cs_getordinate = CsOperation("GEOSCoordSeq_getOrdinate", ordinate=True, get=True)
7993
cs_setordinate = CsOperation("GEOSCoordSeq_setOrdinate", ordinate=True)
8094

81-
# For getting, x, y, z
95+
# For getting, x, y, z, m
8296
cs_getx = CsOperation("GEOSCoordSeq_getX", get=True)
8397
cs_gety = CsOperation("GEOSCoordSeq_getY", get=True)
8498
cs_getz = CsOperation("GEOSCoordSeq_getZ", get=True)
99+
cs_getm = CsOperation("GEOSCoordSeq_getM", get=True)
85100

86-
# For setting, x, y, z
101+
# For setting, x, y, z, m
87102
cs_setx = CsOperation("GEOSCoordSeq_setX")
88103
cs_sety = CsOperation("GEOSCoordSeq_setY")
89104
cs_setz = CsOperation("GEOSCoordSeq_setZ")
105+
cs_setm = CsOperation("GEOSCoordSeq_setM")
90106

91107
# These routines return size & dimensions.
92108
cs_getsize = CsInt("GEOSCoordSeq_getSize")
93109
cs_getdims = CsInt("GEOSCoordSeq_getDimensions")
94110

111+
# Unary Predicates
112+
cs_hasm = CsUnaryPredicate("GEOSCoordSeq_hasM")
113+
95114
cs_is_ccw = GEOSFuncFactory(
96115
"GEOSCoordSeq_isCCW", restype=c_int, argtypes=[CS_PTR, POINTER(c_byte)]
97116
)

tests/gis_tests/geos_tests/test_coordseq.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from django.contrib.gis.geos import LineString
1+
import math
2+
from unittest import skipIf
3+
from unittest.mock import patch
4+
5+
from django.contrib.gis.geos import GEOSGeometry, LineString
6+
from django.contrib.gis.geos import prototypes as capi
7+
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
8+
from django.contrib.gis.geos.libgeos import geos_version_tuple
29
from django.test import SimpleTestCase
310

411

@@ -13,3 +20,130 @@ def test_getitem(self):
1320
with self.subTest(i):
1421
with self.assertRaisesMessage(IndexError, msg):
1522
coord_seq[i]
23+
24+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
25+
def test_has_m(self):
26+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
27+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
28+
self.assertIs(coord_seq.hasm, True)
29+
30+
geom = GEOSGeometry("POINT Z (1 2 3)")
31+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
32+
self.assertIs(coord_seq.hasm, False)
33+
34+
geom = GEOSGeometry("POINT M (1 2 3)")
35+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
36+
self.assertIs(coord_seq.hasm, True)
37+
38+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
39+
def test_get_set_m(self):
40+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
41+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
42+
self.assertEqual(coord_seq.tuple, (1, 2, 3, 4))
43+
self.assertEqual(coord_seq.getM(0), 4)
44+
coord_seq.setM(0, 10)
45+
self.assertEqual(coord_seq.tuple, (1, 2, 3, 10))
46+
self.assertEqual(coord_seq.getM(0), 10)
47+
48+
geom = GEOSGeometry("POINT M (1 2 4)")
49+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
50+
self.assertEqual(coord_seq.tuple, (1, 2, 4))
51+
self.assertEqual(coord_seq.getM(0), 4)
52+
coord_seq.setM(0, 10)
53+
self.assertEqual(coord_seq.tuple, (1, 2, 10))
54+
self.assertEqual(coord_seq.getM(0), 10)
55+
self.assertIs(math.isnan(coord_seq.getZ(0)), True)
56+
57+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
58+
def test_setitem(self):
59+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
60+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
61+
coord_seq[0] = (10, 20, 30, 40)
62+
self.assertEqual(coord_seq.tuple, (10, 20, 30, 40))
63+
64+
geom = GEOSGeometry("POINT M (1 2 4)")
65+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
66+
coord_seq[0] = (10, 20, 40)
67+
self.assertEqual(coord_seq.tuple, (10, 20, 40))
68+
self.assertEqual(coord_seq.getM(0), 40)
69+
self.assertIs(math.isnan(coord_seq.getZ(0)), True)
70+
71+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
72+
def test_kml_m_dimension(self):
73+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
74+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
75+
self.assertEqual(coord_seq.kml, "<coordinates>1.0,2.0,3.0</coordinates>")
76+
geom = GEOSGeometry("POINT M (1 2 4)")
77+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
78+
self.assertEqual(coord_seq.kml, "<coordinates>1.0,2.0,0</coordinates>")
79+
80+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
81+
def test_clone_m_dimension(self):
82+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
83+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
84+
clone = coord_seq.clone()
85+
self.assertEqual(clone.tuple, (1, 2, 3, 4))
86+
self.assertIs(clone.hasz, True)
87+
self.assertIs(clone.hasm, True)
88+
89+
geom = GEOSGeometry("POINT M (1 2 4)")
90+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
91+
clone = coord_seq.clone()
92+
self.assertEqual(clone.tuple, (1, 2, 4))
93+
self.assertIs(clone.hasz, False)
94+
self.assertIs(clone.hasm, True)
95+
96+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
97+
def test_dims(self):
98+
geom = GEOSGeometry("POINT ZM (1 2 3 4)")
99+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
100+
self.assertEqual(coord_seq.dims, 4)
101+
102+
geom = GEOSGeometry("POINT M (1 2 4)")
103+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
104+
self.assertEqual(coord_seq.dims, 3)
105+
106+
geom = GEOSGeometry("POINT Z (1 2 3)")
107+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
108+
self.assertEqual(coord_seq.dims, 3)
109+
110+
geom = GEOSGeometry("POINT (1 2)")
111+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
112+
self.assertEqual(coord_seq.dims, 2)
113+
114+
def test_size(self):
115+
geom = GEOSGeometry("POINT (1 2)")
116+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
117+
self.assertEqual(coord_seq.size, 1)
118+
119+
geom = GEOSGeometry("POINT M (1 2 4)")
120+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
121+
self.assertEqual(coord_seq.size, 1)
122+
123+
@skipIf(geos_version_tuple() < (3, 14), "GEOS M support requires 3.14+")
124+
def test_iscounterclockwise(self):
125+
geom = GEOSGeometry("LINEARRING ZM (0 0 3 0, 1 0 0 2, 0 1 1 3, 0 0 3 4)")
126+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
127+
self.assertEqual(
128+
coord_seq.tuple,
129+
(
130+
(0.0, 0.0, 3.0, 0.0),
131+
(1.0, 0.0, 0.0, 2.0),
132+
(0.0, 1.0, 1.0, 3.0),
133+
(0.0, 0.0, 3.0, 4.0),
134+
),
135+
)
136+
self.assertIs(coord_seq.is_counterclockwise, True)
137+
138+
def test_m_support_error(self):
139+
geom = GEOSGeometry("POINT M (1 2 4)")
140+
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=True)
141+
msg = "GEOSCoordSeq with an M dimension requires GEOS 3.14+."
142+
143+
# mock geos_version_tuple to be 3.13.13
144+
with patch(
145+
"django.contrib.gis.geos.coordseq.geos_version_tuple",
146+
return_value=(3, 13, 13),
147+
):
148+
with self.assertRaisesMessage(NotImplementedError, msg):
149+
coord_seq.hasm

0 commit comments

Comments
 (0)