Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 72 additions & 11 deletions django/contrib/gis/geos/coordseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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)

Expand All @@ -95,16 +106,36 @@ 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))

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)
Expand All @@ -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."
Expand Down Expand Up @@ -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):
Expand All @@ -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."
Expand All @@ -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 (
"<coordinates>%s</coordinates>"
% "".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"<coordinates>{coordinate_string}</coordinates>"

@property
def tuple(self):
Expand Down
3 changes: 3 additions & 0 deletions django/contrib/gis/geos/prototypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 23 additions & 4 deletions django/contrib/gis/geos/prototypes/coordseq.py
Original file line number Diff line number Diff line change
@@ -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. ##
Expand Down Expand Up @@ -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.
Expand All @@ -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)]
)
1 change: 0 additions & 1 deletion docs/topics/email.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 135 additions & 1 deletion tests/gis_tests/geos_tests/test_coordseq.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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, "<coordinates>1.0,2.0,3.0</coordinates>")
geom = GEOSGeometry("POINT M (1 2 4)")
coord_seq = GEOSCoordSeq(capi.get_cs(geom.ptr), z=False)
self.assertEqual(coord_seq.kml, "<coordinates>1.0,2.0,0</coordinates>")

@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