Skip to content

Commit

Permalink
Refs #35058 -- Added support for measured geometries to GDAL Point.
Browse files Browse the repository at this point in the history
Co-authored-by: Nick Pope <[email protected]>
  • Loading branch information
2 people authored and felixxm committed Jan 25, 2024
1 parent a702a07 commit 3f6d939
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 7 deletions.
44 changes: 40 additions & 4 deletions django/contrib/gis/gdal/geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,22 @@ def set_3d(self, value):
else:
raise ValueError(f"Input to 'set_3d' must be a boolean, got '{value!r}'.")

@property
def is_measured(self):
"""Return True if the geometry has M coordinates."""
return capi.is_measured(self.ptr)

def set_measured(self, value):
"""Set if this geometry has M coordinates."""
if value is True:
capi.set_measured(self.ptr, 1)
elif value is False:
capi.set_measured(self.ptr, 0)
else:
raise ValueError(
f"Input to 'set_measured' must be a boolean, got '{value!r}'."
)

# #### SpatialReference-related Properties ####

# The SRS property
Expand Down Expand Up @@ -386,14 +402,22 @@ def wkb(self):
sz = self.wkb_size
# Creating the unsigned character buffer, and passing it in by reference.
buf = (c_ubyte * sz)()
capi.to_wkb(self.ptr, byteorder, byref(buf))
# For backward compatibility, export old-style 99-402 extended
# dimension types when geometry does not have an M dimension.
# https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_ExportToWkb12OGRGeometryH15OGRwkbByteOrderPh
to_wkb = capi.to_iso_wkb if self.is_measured else capi.to_wkb
to_wkb(self.ptr, byteorder, byref(buf))
# Returning a buffer of the string at the pointer.
return memoryview(string_at(buf, sz))

@property
def wkt(self):
"Return the WKT representation of the Geometry."
return capi.to_wkt(self.ptr, byref(c_char_p()))
# For backward compatibility, export old-style 99-402 extended
# dimension types when geometry does not have an M dimension.
# https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_ExportToWkt12OGRGeometryHPPc
to_wkt = capi.to_iso_wkt if self.is_measured else capi.to_wkt
return to_wkt(self.ptr, byref(c_char_p()))

@property
def ewkt(self):
Expand Down Expand Up @@ -568,11 +592,21 @@ def z(self):
if self.is_3d:
return capi.getz(self.ptr, 0)

@property
def m(self):
"""Return the M coordinate for this Point."""
if self.is_measured:
return capi.getm(self.ptr, 0)

@property
def tuple(self):
"Return the tuple of this point."
if self.is_3d and self.is_measured:
return self.x, self.y, self.z, self.m
if self.is_3d:
return (self.x, self.y, self.z)
return self.x, self.y, self.z
if self.is_measured:
return self.x, self.y, self.m
return self.x, self.y

coords = tuple
Expand Down Expand Up @@ -753,7 +787,9 @@ class MultiPolygon(GeometryCollection):
6: MultiPolygon,
7: GeometryCollection,
101: LinearRing,
1 + OGRGeomType.wkb25bit: Point,
2001: Point, # POINT M
3001: Point, # POINT ZM
1 + OGRGeomType.wkb25bit: Point, # POINT Z
2 + OGRGeomType.wkb25bit: LineString,
3 + OGRGeomType.wkb25bit: Polygon,
4 + OGRGeomType.wkb25bit: MultiPoint,
Expand Down
9 changes: 8 additions & 1 deletion django/contrib/gis/gdal/prototypes/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def topology_func(f):
getx = pnt_func(lgdal.OGR_G_GetX)
gety = pnt_func(lgdal.OGR_G_GetY)
getz = pnt_func(lgdal.OGR_G_GetZ)
getm = pnt_func(lgdal.OGR_G_GetM)

# Geometry creation routines.
if GDAL_VERSION >= (3, 3):
Expand Down Expand Up @@ -82,6 +83,8 @@ def topology_func(f):
geom_union = geom_output(lgdal.OGR_G_Union, [c_void_p, c_void_p])
is_3d = bool_output(lgdal.OGR_G_Is3D, [c_void_p])
set_3d = void_output(lgdal.OGR_G_Set3D, [c_void_p, c_int], errcheck=False)
is_measured = bool_output(lgdal.OGR_G_IsMeasured, [c_void_p])
set_measured = void_output(lgdal.OGR_G_SetMeasured, [c_void_p, c_int], errcheck=False)

# Geometry modification routines.
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])
Expand All @@ -94,9 +97,13 @@ def topology_func(f):
to_wkb = void_output(
lgdal.OGR_G_ExportToWkb, None, errcheck=True
) # special handling for WKB.
to_iso_wkb = void_output(lgdal.OGR_G_ExportToIsoWkb, None, errcheck=True)
to_wkt = string_output(
lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)], decoding="ascii"
)
to_iso_wkt = string_output(
lgdal.OGR_G_ExportToIsoWkt, [c_void_p, POINTER(c_char_p)], decoding="ascii"
)
to_gml = string_output(
lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True, decoding="ascii"
)
Expand All @@ -115,7 +122,7 @@ def topology_func(f):
get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p])
get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p])
get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p])
get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p])
get_coord_dim = int_output(lgdal.OGR_G_CoordinateDimension, [c_void_p])
set_coord_dim = void_output(
lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False
)
Expand Down
4 changes: 4 additions & 0 deletions django/contrib/gis/utils/layermapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,10 @@ def verify_geom(self, geom, model_field):
if necessary (for example if the model field is MultiPolygonField while
the mapped shapefile only contains Polygons).
"""
# Measured geometries are not yet supported by GeoDjango models.
if geom.is_measured:
geom.set_measured(False)

# Downgrade a 3D geom to a 2D one, if necessary.
if self.coord_dim == 2 and geom.is_3d:
geom.set_3d(False)
Expand Down
33 changes: 33 additions & 0 deletions docs/ref/contrib/gis/gdal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,27 @@ coordinate transformation:
>>> p.wkt
"POINT (1 2)"

.. attribute:: is_measured

.. versionadded:: 5.1

A boolean indicating if this geometry has M coordinates.

.. method:: set_measured(value)

.. versionadded:: 5.1

A method to add or remove the M coordinate dimension.

.. code-block:: pycon

>>> p = OGRGeometry("POINT (1 2)")
>>> p.is_measured
False
>>> p.set_measured(True)
>>> p.wkt
"POINT M (1 2 0)"

.. attribute:: geom_count

Returns the number of elements in this geometry:
Expand Down Expand Up @@ -864,6 +885,18 @@ coordinate transformation:
>>> OGRGeometry("POINT (1 2 3)").z
3.0

.. attribute:: m

.. versionadded:: 5.1

Returns the M coordinate of this point, or ``None`` if the Point does not
have an M coordinate:

.. code-block:: pycon

>>> OGRGeometry("POINT ZM (1 2 3 4)").m
4.0

.. class:: LineString

.. attribute:: x
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Minor features
* The new :meth:`.OGRGeometry.set_3d` method allows addition and removal of the
``Z`` coordinate dimension.

* :class:`~django.contrib.gis.gdal.OGRGeometry` and
:class:`~django.contrib.gis.gdal.Point` now support measured geometries
via the new :attr:`.OGRGeometry.is_measured` and :attr:`.Point.m` properties,
and the :meth:`.OGRGeometry.set_measured` method.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
69 changes: 67 additions & 2 deletions tests/gis_tests/gdal_tests/test_geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ def test_geometry_types(self):
("PolyhedralSurface Z", 1015, False),
("TIN Z", 1016, False),
("Triangle Z", 1017, False),
("Point M", 2001, False),
("Point M", 2001, True),
("LineString M", 2002, False),
("Polygon M", 2003, False),
("MultiPoint M", 2004, False),
Expand All @@ -687,7 +687,7 @@ def test_geometry_types(self):
("PolyhedralSurface M", 2015, False),
("TIN M", 2016, False),
("Triangle M", 2017, False),
("Point ZM", 3001, False),
("Point ZM", 3001, True),
("LineString ZM", 3002, False),
("Polygon ZM", 3003, False),
("MultiPoint ZM", 3004, False),
Expand Down Expand Up @@ -812,6 +812,71 @@ def test_wkt_and_wkb_output(self):
self.assertEqual(g.wkt, geom)
self.assertEqual(g.wkb.hex(), wkb)

def test_measure_is_measure_and_set_measure(self):
geom = OGRGeometry("POINT (1 2 3)")
self.assertIs(geom.is_measured, False)
geom.set_measured(True)
self.assertIs(geom.is_measured, True)
self.assertEqual(geom.wkt, "POINT ZM (1 2 3 0)")
geom.set_measured(False)
self.assertIs(geom.is_measured, False)
self.assertEqual(geom.wkt, "POINT (1 2 3)")
msg = "Input to 'set_measured' must be a boolean, got 'None'"
with self.assertRaisesMessage(ValueError, msg):
geom.set_measured(None)

def test_point_m_coordinate(self):
geom = OGRGeometry("POINT ZM (1 2 3 4)")
self.assertEqual(geom.m, 4)
geom = OGRGeometry("POINT (1 2 3 4)")
self.assertEqual(geom.m, 4)
geom = OGRGeometry("POINT M (1 2 3)")
self.assertEqual(geom.m, 3)
geom = OGRGeometry("POINT Z (1 2 3)")
self.assertEqual(geom.m, None)

def test_point_m_tuple(self):
geom = OGRGeometry("POINT ZM (1 2 3 4)")
self.assertEqual(geom.tuple, (geom.x, geom.y, geom.z, geom.m))
geom = OGRGeometry("POINT M (1 2 3)")
self.assertEqual(geom.tuple, (geom.x, geom.y, geom.m))
geom = OGRGeometry("POINT Z (1 2 3)")
self.assertEqual(geom.tuple, (geom.x, geom.y, geom.z))
geom = OGRGeometry("POINT (1 2 3)")
self.assertEqual(geom.tuple, (geom.x, geom.y, geom.z))

def test_point_m_wkt_wkb(self):
wkt = "POINT ZM (1 2 3 4)"
geom = OGRGeometry(wkt)
self.assertEqual(geom.wkt, wkt)
self.assertEqual(
geom.wkb.hex(),
"01b90b0000000000000000f03f00000000000000"
"4000000000000008400000000000001040",
)
wkt = "POINT M (1 2 3)"
geom = OGRGeometry(wkt)
self.assertEqual(geom.wkt, wkt)
self.assertEqual(
geom.wkb.hex(),
"01d1070000000000000000f03f00000000000000400000000000000840",
)

def test_point_m_dimension_types(self):
geom = OGRGeometry("POINT ZM (1 2 3 4)")
self.assertEqual(geom.geom_type.name, "PointZM")
self.assertEqual(geom.geom_type.num, 3001)
geom = OGRGeometry("POINT M (1 2 3)")
self.assertEqual(geom.geom_type.name, "PointM")
self.assertEqual(geom.geom_type.num, 2001)

def test_point_m_dimension_geos(self):
"""GEOSGeometry does not yet support the M dimension."""
geom = OGRGeometry("POINT ZM (1 2 3 4)")
self.assertEqual(geom.geos.wkt, "POINT Z (1 2 3)")
geom = OGRGeometry("POINT M (1 2 3)")
self.assertEqual(geom.geos.wkt, "POINT (1 2)")


class DeprecationTests(SimpleTestCase):
def test_coord_setter_deprecation(self):
Expand Down

0 comments on commit 3f6d939

Please sign in to comment.