Refs #35058 -- Added support for measured geometries to GDAL Point.

Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
David Smith 2024-01-23 08:12:03 +00:00 committed by Mariusz Felisiak
parent a702a0773d
commit 3f6d939c62
6 changed files with 157 additions and 7 deletions

View File

@ -287,6 +287,22 @@ class OGRGeometry(GDALBase):
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
@ -386,14 +402,22 @@ class OGRGeometry(GDALBase):
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):
@ -568,11 +592,21 @@ class Point(OGRGeometry):
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
@ -753,7 +787,9 @@ GEO_CLASSES = {
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,

View File

@ -51,6 +51,7 @@ to_kml = string_output(
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):
@ -82,6 +83,8 @@ geom_sym_diff = geom_output(lgdal.OGR_G_SymmetricDifference, [c_void_p, c_void_p
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])
@ -94,9 +97,13 @@ destroy_geom = void_output(lgdal.OGR_G_DestroyGeometry, [c_void_p], errcheck=Fal
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"
)
@ -115,7 +122,7 @@ get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p])
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
)

View File

@ -488,6 +488,10 @@ class LayerMapping:
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)

View File

@ -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:
@ -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

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -672,7 +672,7 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
("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),
@ -687,7 +687,7 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
("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),
@ -812,6 +812,71 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
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):