This commit is contained in:
2025-01-26 19:24:23 -08:00
parent 32cd60e92b
commit d1dde0dbc6
4155 changed files with 29170 additions and 216373 deletions

View File

@@ -728,20 +728,26 @@ def set_precision(geometry, grid_size, mode="valid_output", **kwargs):
By default, geometries use double precision coordinates (grid_size = 0).
Coordinates will be rounded if a precision grid is less precise than the
input geometry. Duplicated vertices will be dropped from lines and
Coordinates will be rounded if the precision grid specified is less precise
than the input geometry. Duplicated vertices will be dropped from lines and
polygons for grid sizes greater than 0. Line and polygon geometries may
collapse to empty geometries if all vertices are closer together than
grid_size. Z values, if present, will not be modified.
``grid_size`` or if a polygon becomes significantly narrower than
``grid_size``. Spikes or sections in polygons narrower than ``grid_size``
after rounding the vertices will be removed, which can lead to multipolygons
or empty geometries. Z values, if present, will not be modified.
Note: subsequent operations will always be performed in the precision of
the geometry with higher precision (smaller "grid_size"). That same
precision will be attached to the operation outputs.
Notes:
Also note: input geometries should be geometrically valid; unexpected
results may occur if input geometries are not.
Returns None if geometry is None.
* subsequent operations will always be performed in the precision of the
geometry with higher precision (smaller "grid_size"). That same precision
will be attached to the operation outputs.
* input geometries should be geometrically valid; unexpected results may
occur if input geometries are not.
* the geometry returned will be in
:ref:`mild canonical form <canonical-form>`, and the order of vertices can
change and should not be relied upon.
* returns None if geometry is None.
Parameters
----------
@@ -752,21 +758,24 @@ def set_precision(geometry, grid_size, mode="valid_output", **kwargs):
value is more precise than input geometry, the input geometry will
not be modified.
mode : {'valid_output', 'pointwise', 'keep_collapsed'}, default 'valid_output'
This parameter determines how to handle invalid output geometries. There are three modes:
This parameter determines the way a precision reduction is applied on
the geometry. There are three modes:
1. `'valid_output'` (default): The output is always valid. Collapsed geometry elements
(including both polygons and lines) are removed. Duplicate vertices are removed.
2. `'pointwise'`: Precision reduction is performed pointwise. Output geometry
may be invalid due to collapse or self-intersection. Duplicate vertices are not
removed. In GEOS this option is called NO_TOPO.
1. `'valid_output'` (default): The output is always valid. Collapsed
geometry elements (including both polygons and lines) are removed.
Duplicate vertices are removed.
2. `'pointwise'`: Precision reduction is performed pointwise. Output
geometry may be invalid due to collapse or self-intersection.
Duplicate vertices are not removed. In GEOS this option is called
NO_TOPO.
.. note::
'pointwise' mode requires at least GEOS 3.10. It is accepted in earlier versions,
but the results may be unexpected.
3. `'keep_collapsed'`: Like the default mode, except that collapsed linear geometry
elements are preserved. Collapsed polygonal input elements are removed. Duplicate
vertices are removed.
'pointwise' mode requires at least GEOS 3.10. It is accepted in
earlier versions, but the results may be unexpected.
3. `'keep_collapsed'`: Like the default mode, except that collapsed
linear geometry elements are preserved. Collapsed polygonal input
elements are removed. Duplicate vertices are removed.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.

View File

@@ -37,7 +37,7 @@ from shapely._geometry import (
get_type_id,
)
from shapely.coordinates import get_coordinates
from shapely.predicates import is_empty
from shapely.predicates import is_empty, is_missing
__all__ = ["to_ragged_array", "from_ragged_array"]
@@ -50,7 +50,8 @@ def _get_arrays_point(arr, include_z):
coords = get_coordinates(arr, include_z=include_z)
# empty points are represented by NaNs
empties = is_empty(arr)
# + missing geometries should also be present with some value
empties = is_empty(arr) | is_missing(arr)
if empties.any():
indices = np.nonzero(empties)[0]
indices = indices - np.arange(len(indices))

View File

@@ -8,11 +8,11 @@ import json
version_json = '''
{
"date": "2023-10-12T21:49:31+0200",
"date": "2024-08-19T23:29:00+0200",
"dirty": false,
"error": null,
"full-revisionid": "8d45d434037267ba9b1f1de409d1b5396d6c9219",
"version": "2.0.2"
"full-revisionid": "5a4207d2fb74b25d7bb2fb5d04813f68a3a612f4",
"version": "2.0.6"
}
''' # END VERSION_JSON

View File

@@ -61,15 +61,31 @@ def affine_transform(geom, matrix):
ndim = 2
else:
raise ValueError("'matrix' expects either 6 or 12 coefficients")
if ndim == 2:
A = np.array([[a, b], [d, e]], dtype=float)
off = np.array([xoff, yoff], dtype=float)
else:
A = np.array([[a, b, c], [d, e, f], [g, h, i]], dtype=float)
off = np.array([xoff, yoff, zoff], dtype=float)
# if ndim == 2:
# A = np.array([[a, b], [d, e]], dtype=float)
# off = np.array([xoff, yoff], dtype=float)
# else:
# A = np.array([[a, b, c], [d, e, f], [g, h, i]], dtype=float)
# off = np.array([xoff, yoff, zoff], dtype=float)
def _affine_coords(coords):
return np.matmul(A, coords.T).T + off
# These are equivalent, but unfortunately not robust
# result = np.matmul(coords, A.T) + off
# result = np.matmul(A, coords.T).T + off
# Therefore, manual matrix multiplication is needed
if ndim == 2:
x, y = coords.T
xp = a * x + b * y + xoff
yp = d * x + e * y + yoff
result = np.stack([xp, yp]).T
elif ndim == 3:
x, y, z = coords.T
xp = a * x + b * y + c * z + xoff
yp = d * x + e * y + f * z + yoff
zp = g * x + h * y + i * z + zoff
result = np.stack([xp, yp, zp]).T
return result
return shapely.transform(geom, _affine_coords, include_z=ndim == 3)

View File

@@ -17,8 +17,6 @@ def _oriented_envelope_min_area(geometry, **kwargs):
"""
if geometry is None:
return None
if not hasattr(geometry, "geom_type"):
return np.array([_oriented_envelope_min_area(g) for g in geometry])
if geometry.is_empty:
return shapely.from_wkt("POLYGON EMPTY")
@@ -53,3 +51,8 @@ def _oriented_envelope_min_area(geometry, **kwargs):
# check for the minimum area rectangle and return it
transf_rect, inv_matrix = min(_transformed_rects(), key=lambda r: r[0].area)
return affine_transform(transf_rect, inv_matrix)
_oriented_envelope_min_area_vectorized = np.frompyfunc(
_oriented_envelope_min_area, 1, 1
)

View File

@@ -2,7 +2,7 @@ import numpy as np
from shapely import lib
from shapely._enum import ParamEnum
from shapely.algorithms._oriented_envelope import _oriented_envelope_min_area
from shapely.algorithms._oriented_envelope import _oriented_envelope_min_area_vectorized
from shapely.decorators import multithreading_enabled, requires_geos
__all__ = [
@@ -412,17 +412,18 @@ def delaunay_triangles(geometry, tolerance=0.0, only_edges=False, **kwargs):
--------
>>> from shapely import GeometryCollection, LineString, MultiPoint, Polygon
>>> points = MultiPoint([(50, 30), (60, 30), (100, 100)])
>>> delaunay_triangles(points)
<GEOMETRYCOLLECTION (POLYGON ((50 30, 60 30, 100 100, 50 30)))>
>>> delaunay_triangles(points).normalize()
<GEOMETRYCOLLECTION (POLYGON ((50 30, 100 100, 60 30, 50 30)))>
>>> delaunay_triangles(points, only_edges=True)
<MULTILINESTRING ((50 30, 100 100), (50 30, 60 30), ...>
>>> delaunay_triangles(MultiPoint([(50, 30), (51, 30), (60, 30), (100, 100)]), \
tolerance=2)
<GEOMETRYCOLLECTION (POLYGON ((50 30, 60 30, 100 100, 50 30)))>
>>> delaunay_triangles(Polygon([(50, 30), (60, 30), (100, 100), (50, 30)]))
<GEOMETRYCOLLECTION (POLYGON ((50 30, 60 30, 100 100, 50 30)))>
>>> delaunay_triangles(LineString([(50, 30), (60, 30), (100, 100)]))
<GEOMETRYCOLLECTION (POLYGON ((50 30, 60 30, 100 100, 50 30)))>
tolerance=2).normalize()
<GEOMETRYCOLLECTION (POLYGON ((50 30, 100 100, 60 30, 50 30)))>
>>> delaunay_triangles(Polygon([(50, 30), (60, 30), (100, 100), (50, 30)]))\
.normalize()
<GEOMETRYCOLLECTION (POLYGON ((50 30, 100 100, 60 30, 50 30)))>
>>> delaunay_triangles(LineString([(50, 30), (60, 30), (100, 100)])).normalize()
<GEOMETRYCOLLECTION (POLYGON ((50 30, 100 100, 60 30, 50 30)))>
>>> delaunay_triangles(GeometryCollection([]))
<GEOMETRYCOLLECTION EMPTY>
"""
@@ -533,11 +534,11 @@ def make_valid(geometry, **kwargs):
@multithreading_enabled
def normalize(geometry, **kwargs):
"""Converts Geometry to normal form (or canonical form).
"""Converts Geometry to strict normal form (or canonical form).
This method orders the coordinates, rings of a polygon and parts of
multi geometries consistently. Typically useful for testing purposes
(for example in combination with ``equals_exact``).
In :ref:`strict canonical form <canonical-form>`, the coordinates, rings of a polygon and
parts of multi geometries are ordered consistently. Typically useful for testing
purposes (for example in combination with ``equals_exact``).
Parameters
----------
@@ -1028,7 +1029,7 @@ def oriented_envelope(geometry, **kwargs):
<POLYGON EMPTY>
"""
if lib.geos_version < (3, 12, 0):
f = _oriented_envelope_min_area
f = _oriented_envelope_min_area_vectorized
else:
f = _oriented_envelope_geos
return f(geometry, **kwargs)

View File

@@ -46,8 +46,13 @@ class CoordinateSequence:
else:
raise TypeError("key must be an index or slice")
def __array__(self, dtype=None):
return self._coords
def __array__(self, dtype=None, copy=None):
if copy is False:
raise ValueError("`copy=False` isn't supported. A copy is always created.")
elif copy is True:
return self._coords.copy()
else:
return self._coords
@property
def xy(self):

View File

@@ -353,7 +353,7 @@ def multipoints(geometries, indices=None, out=None, **kwargs):
):
geometries = points(geometries)
if indices is None:
return lib.create_collection(geometries, typ, out=out, **kwargs)
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@@ -390,7 +390,7 @@ def multilinestrings(geometries, indices=None, out=None, **kwargs):
geometries = linestrings(geometries)
if indices is None:
return lib.create_collection(geometries, typ, out=out, **kwargs)
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@@ -426,7 +426,7 @@ def multipolygons(geometries, indices=None, out=None, **kwargs):
):
geometries = polygons(geometries)
if indices is None:
return lib.create_collection(geometries, typ, out=out, **kwargs)
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@@ -457,7 +457,7 @@ def geometrycollections(geometries, indices=None, out=None, **kwargs):
"""
typ = GeometryType.GEOMETRYCOLLECTION
if indices is None:
return lib.create_collection(geometries, typ, out=out, **kwargs)
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@@ -511,7 +511,7 @@ def destroy_prepared(geometry, **kwargs):
Parameters
----------
geometry : Geometry or array_like
Geometries are changed inplace
Geometries are changed in-place
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.

View File

@@ -39,7 +39,7 @@ class requires_geos:
# Insert the message at the first double newline
position = doc.find("\n\n") + 2
# Figure out the indentation level
indent = 2
indent = 0
while True:
if doc[position + indent] == " ":
indent += 1

View File

@@ -85,12 +85,12 @@ class Point(BaseGeometry):
@property
def x(self):
"""Return x coordinate."""
return shapely.get_x(self)
return float(shapely.get_x(self))
@property
def y(self):
"""Return y coordinate."""
return shapely.get_y(self)
return float(shapely.get_y(self))
@property
def z(self):

View File

@@ -10,7 +10,7 @@ from shapely.geometry.base import BaseGeometry
from shapely.geometry.linestring import LineString
from shapely.geometry.point import Point
__all__ = ["Polygon", "LinearRing"]
__all__ = ["orient", "Polygon", "LinearRing"]
def _unpickle_linearring(wkb):

View File

@@ -139,12 +139,12 @@ def to_wkb(
----------
geometry : Geometry or array_like
hex : bool, default False
If true, export the WKB as a hexidecimal string. The default is to
If true, export the WKB as a hexadecimal string. The default is to
return a binary bytes object.
output_dimension : int, default 3
The output dimension for the WKB. Supported values are 2 and 3.
Specifying 3 means that up to 3 dimensions will be written but 2D
geometries will still be represented as 2D in the WKB represenation.
geometries will still be represented as 2D in the WKB representation.
byte_order : int, default -1
Defaults to native machine byte order (-1). Use 0 to force big endian
and 1 for little endian.

View File

@@ -1,7 +1,7 @@
"""
Plot single geometries using Matplotlib.
Note: this module is experimental, and mainly targetting (interactive)
Note: this module is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
"""
@@ -38,7 +38,7 @@ def patch_from_polygon(polygon, **kwargs):
"""
Gets a Matplotlib patch from a (Multi)Polygon.
Note: this function is experimental, and mainly targetting (interactive)
Note: this function is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
@@ -69,7 +69,7 @@ def plot_polygon(
"""
Plot a (Multi)Polygon.
Note: this function is experimental, and mainly targetting (interactive)
Note: this function is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
@@ -132,7 +132,7 @@ def plot_line(line, ax=None, add_points=True, color=None, linewidth=2, **kwargs)
"""
Plot a (Multi)LineString/LinearRing.
Note: this function is experimental, and mainly targetting (interactive)
Note: this function is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
@@ -144,7 +144,7 @@ def plot_line(line, ax=None, add_points=True, color=None, linewidth=2, **kwargs)
add_points : bool, default True
If True, also plot the coordinates (vertices) as points.
color : matplotlib color specification
Color for the line (edgecolor under the hood) and pointes.
Color for the line (edgecolor under the hood) and points.
linewidth : float, default 2
The line width for the polygon boundary.
**kwargs

View File

@@ -81,7 +81,7 @@ def is_ccw(geometry, **kwargs):
Parameters
----------
geometry : Geometry or array_like
This function will return False for non-linear goemetries and for
This function will return False for non-linear geometries and for
lines with fewer than 4 points (including the closing point).
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
@@ -748,7 +748,7 @@ def equals(a, b, **kwargs):
def intersects(a, b, **kwargs):
"""Returns True if A and B share any portion of space.
Intersects implies that overlaps, touches and within are True.
Intersects implies that overlaps, touches, covers, or within are True.
Parameters
----------

View File

@@ -403,7 +403,9 @@ def union_all(geometries, grid_size=None, axis=None, **kwargs):
geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim)
# create_collection acts on the inner axis
collections = lib.create_collection(geometries, GeometryType.GEOMETRYCOLLECTION)
collections = lib.create_collection(
geometries, np.intc(GeometryType.GEOMETRYCOLLECTION)
)
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
@@ -501,5 +503,7 @@ def coverage_union_all(geometries, axis=None, **kwargs):
np.asarray(geometries), axis=axis, start=geometries.ndim
)
# create_collection acts on the inner axis
collections = lib.create_collection(geometries, GeometryType.GEOMETRYCOLLECTION)
collections = lib.create_collection(
geometries, np.intc(GeometryType.GEOMETRYCOLLECTION)
)
return lib.coverage_union(collections, **kwargs)

View File

@@ -106,8 +106,8 @@ def assert_geometries_equal(
if normalize:
x = shapely.normalize(x)
y = shapely.normalize(y)
x = np.array(x, copy=False)
y = np.array(y, copy=False)
x = np.asarray(x)
y = np.asarray(y)
is_scalar = x.ndim == 0 or y.ndim == 0

View File

@@ -92,3 +92,34 @@ def ignore_invalid(condition=True):
with ignore_invalid():
line_string_nan = shapely.LineString([(np.nan, np.nan), (np.nan, np.nan)])
class ArrayLike:
"""
Simple numpy Array like class that implements the
ufunc protocol.
"""
def __init__(self, array):
self._array = np.asarray(array)
def __len__(self):
return len(self._array)
def __getitem(self, key):
return self._array[key]
def __iter__(self):
return self._array.__iter__()
def __array__(self):
return np.asarray(self._array)
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
if method == "__call__":
inputs = [
arg._array if isinstance(arg, self.__class__) else arg for arg in inputs
]
return self.__class__(ufunc(*inputs, **kwargs))
else:
return NotImplemented

View File

@@ -2,6 +2,7 @@ import numpy as np
import pytest
from shapely import LineString
from shapely.tests.common import line_string, line_string_z, point, point_z
class TestCoords:
@@ -84,3 +85,18 @@ class TestXY:
assert list(x) == [0.0, 1.0]
assert len(y) == 2
assert list(y) == [0.0, 1.0]
@pytest.mark.parametrize("geom", [point, point_z, line_string, line_string_z])
def test_coords_array_copy(geom):
"""Test CoordinateSequence.__array__ method."""
coord_seq = geom.coords
assert np.array(coord_seq) is not np.array(coord_seq)
assert np.array(coord_seq, copy=True) is not np.array(coord_seq, copy=True)
# Behaviour of copy=False is different between NumPy 1.x and 2.x
if int(np.version.short_version.split(".", 1)[0]) >= 2:
with pytest.raises(ValueError, match="A copy is always created"):
np.array(coord_seq, copy=False)
else:
assert np.array(coord_seq, copy=False) is np.array(coord_seq, copy=False)

View File

@@ -47,7 +47,7 @@ def test_format_point():
("g", xy2, "POINT (-169.910918 -18.997564)", False),
("0.2g", xy2, "POINT (-169.91 -19)", False),
]
# without precsions test GEOS rounding_precision=-1; different than Python
# without precisions test GEOS rounding_precision=-1; different than Python
test_list += [
("f", (1, 2), f"POINT ({1:.16f} {2:.16f})", False),
("F", xyz3, "POINT Z ({:.16f} {:.16f} {:.16f})".format(*xyz3), False),
@@ -84,23 +84,28 @@ def test_format_polygon():
assert format(poly, "X") == poly.wkb_hex
# Use f-strings with extra characters and rounding precision
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 -0.00, -7.07 7.07, -0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
if geos_version < (3, 13, 0):
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 -0.00, -7.07 7.07, -0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
else:
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 0.00, -7.07 7.07, 0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
# 'g' format varies depending on GEOS version
if geos_version < (3, 10, 0):
expected_2G = (
assert f"{poly:.2G}" == (
"POLYGON ((10 0, 7.1 -7.1, 1.6E-14 -10, -7.1 -7.1, "
"-10 -3.2E-14, -7.1 7.1, -4.6E-14 10, 7.1 7.1, 10 0))"
)
else:
expected_2G = (
assert f"{poly:.2G}" == (
"POLYGON ((10 0, 7.07 -7.07, 0 -10, -7.07 -7.07, "
"-10 0, -7.07 7.07, 0 10, 7.07 7.07, 10 0))"
)
assert f"{poly:.2G}" == expected_2G
# check empty
empty = Polygon()

View File

@@ -101,7 +101,9 @@ class TestPoint:
# Test 2D points
p = Point(1.0, 2.0)
assert p.x == 1.0
assert type(p.x) is float
assert p.y == 2.0
assert type(p.y) is float
assert p.coords[:] == [(1.0, 2.0)]
assert str(p) == p.wkt
assert p.has_z is False
@@ -114,6 +116,7 @@ class TestPoint:
assert str(p) == p.wkt
assert p.has_z is True
assert p.z == 3.0
assert type(p.z) is float
# Coordinate access
p = Point((3.0, 4.0))

Some files were not shown because too many files have changed in this diff Show More