refactor: excel parse

This commit is contained in:
Blizzard
2026-04-16 10:01:11 +08:00
parent 680ecc320f
commit f62f95ec02
7941 changed files with 2899112 additions and 0 deletions
@@ -0,0 +1,33 @@
from shapely.lib import GEOSException # NOQA
from shapely.lib import Geometry # NOQA
from shapely.lib import geos_version, geos_version_string # NOQA
from shapely.lib import geos_capi_version, geos_capi_version_string # NOQA
from shapely.errors import setup_signal_checks # NOQA
from shapely._geometry import * # NOQA
from shapely.creation import * # NOQA
from shapely.constructive import * # NOQA
from shapely.predicates import * # NOQA
from shapely.measurement import * # NOQA
from shapely.set_operations import * # NOQA
from shapely.linear import * # NOQA
from shapely.coordinates import * # NOQA
from shapely.strtree import * # NOQA
from shapely.io import * # NOQA
# Submodule always needs to be imported to ensure Geometry subclasses are registered
from shapely.geometry import ( # NOQA
Point,
LineString,
Polygon,
MultiPoint,
MultiLineString,
MultiPolygon,
GeometryCollection,
LinearRing,
)
from shapely import _version
__version__ = _version.get_versions()["version"]
setup_signal_checks()
@@ -0,0 +1,23 @@
from enum import IntEnum
class ParamEnum(IntEnum):
"""Wraps IntEnum to provide validation of a requested item.
Intended for enums used for function parameters.
Use enum.get_value(item) for this behavior instead of builtin enum[item].
"""
@classmethod
def get_value(cls, item):
"""Validate incoming item and raise a ValueError with valid options if not present."""
try:
return cls[item].value
except KeyError:
valid_options = {e.name for e in cls}
raise ValueError(
"'{}' is not a valid option, must be one of '{}'".format(
item, "', '".join(valid_options)
)
)
@@ -0,0 +1,875 @@
import warnings
from enum import IntEnum
import numpy as np
from shapely import _geometry_helpers, geos_version, lib
from shapely._enum import ParamEnum
from shapely.decorators import multithreading_enabled, requires_geos
__all__ = [
"GeometryType",
"get_type_id",
"get_dimensions",
"get_coordinate_dimension",
"get_num_coordinates",
"get_srid",
"set_srid",
"get_x",
"get_y",
"get_z",
"get_exterior_ring",
"get_num_points",
"get_num_interior_rings",
"get_num_geometries",
"get_point",
"get_interior_ring",
"get_geometry",
"get_parts",
"get_rings",
"get_precision",
"set_precision",
"force_2d",
"force_3d",
]
class GeometryType(IntEnum):
"""The enumeration of GEOS geometry types"""
MISSING = -1
POINT = 0
LINESTRING = 1
LINEARRING = 2
POLYGON = 3
MULTIPOINT = 4
MULTILINESTRING = 5
MULTIPOLYGON = 6
GEOMETRYCOLLECTION = 7
# generic
@multithreading_enabled
def get_type_id(geometry, **kwargs):
"""Returns the type ID of a geometry.
- None (missing) is -1
- POINT is 0
- LINESTRING is 1
- LINEARRING is 2
- POLYGON is 3
- MULTIPOINT is 4
- MULTILINESTRING is 5
- MULTIPOLYGON is 6
- GEOMETRYCOLLECTION is 7
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
GeometryType
Examples
--------
>>> from shapely import LineString, Point
>>> get_type_id(LineString([(0, 0), (1, 1), (2, 2), (3, 3)]))
1
>>> get_type_id([Point(1, 2), Point(2, 3)]).tolist()
[0, 0]
"""
return lib.get_type_id(geometry, **kwargs)
@multithreading_enabled
def get_dimensions(geometry, **kwargs):
"""Returns the inherent dimensionality of a geometry.
The inherent dimension is 0 for points, 1 for linestrings and linearrings,
and 2 for polygons. For geometrycollections it is the max of the containing
elements. Empty collections and None values return -1.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import GeometryCollection, Point, Polygon
>>> point = Point(0, 0)
>>> get_dimensions(point)
0
>>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
>>> get_dimensions(polygon)
2
>>> get_dimensions(GeometryCollection([point, polygon]))
2
>>> get_dimensions(GeometryCollection([]))
-1
>>> get_dimensions(None)
-1
"""
return lib.get_dimensions(geometry, **kwargs)
@multithreading_enabled
def get_coordinate_dimension(geometry, **kwargs):
"""Returns the dimensionality of the coordinates in a geometry (2 or 3).
Returns -1 for missing geometries (``None`` values). Note that if the first Z
coordinate equals ``nan``, this function will return ``2``.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import Point
>>> get_coordinate_dimension(Point(0, 0))
2
>>> get_coordinate_dimension(Point(0, 0, 1))
3
>>> get_coordinate_dimension(None)
-1
>>> get_coordinate_dimension(Point(0, 0, float("nan")))
2
"""
return lib.get_coordinate_dimension(geometry, **kwargs)
@multithreading_enabled
def get_num_coordinates(geometry, **kwargs):
"""Returns the total number of coordinates in a geometry.
Returns 0 for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import GeometryCollection, LineString, Point
>>> point = Point(0, 0)
>>> get_num_coordinates(point)
1
>>> get_num_coordinates(Point(0, 0, 0))
1
>>> line = LineString([(0, 0), (1, 1)])
>>> get_num_coordinates(line)
2
>>> get_num_coordinates(GeometryCollection([point, line]))
3
>>> get_num_coordinates(None)
0
"""
return lib.get_num_coordinates(geometry, **kwargs)
@multithreading_enabled
def get_srid(geometry, **kwargs):
"""Returns the SRID of a geometry.
Returns -1 for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
set_srid
Examples
--------
>>> from shapely import Point
>>> point = Point(0, 0)
>>> get_srid(point)
0
>>> with_srid = set_srid(point, 4326)
>>> get_srid(with_srid)
4326
"""
return lib.get_srid(geometry, **kwargs)
@multithreading_enabled
def set_srid(geometry, srid, **kwargs):
"""Returns a geometry with its SRID set.
Parameters
----------
geometry : Geometry or array_like
srid : int
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_srid
Examples
--------
>>> from shapely import Point
>>> point = Point(0, 0)
>>> get_srid(point)
0
>>> with_srid = set_srid(point, 4326)
>>> get_srid(with_srid)
4326
"""
return lib.set_srid(geometry, np.intc(srid), **kwargs)
# points
@multithreading_enabled
def get_x(point, **kwargs):
"""Returns the x-coordinate of a point
Parameters
----------
point : Geometry or array_like
Non-point geometries will result in NaN being returned.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_y, get_z
Examples
--------
>>> from shapely import MultiPoint, Point
>>> get_x(Point(1, 2))
1.0
>>> get_x(MultiPoint([(1, 1), (1, 2)]))
nan
"""
return lib.get_x(point, **kwargs)
@multithreading_enabled
def get_y(point, **kwargs):
"""Returns the y-coordinate of a point
Parameters
----------
point : Geometry or array_like
Non-point geometries will result in NaN being returned.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_x, get_z
Examples
--------
>>> from shapely import MultiPoint, Point
>>> get_y(Point(1, 2))
2.0
>>> get_y(MultiPoint([(1, 1), (1, 2)]))
nan
"""
return lib.get_y(point, **kwargs)
@requires_geos("3.7.0")
@multithreading_enabled
def get_z(point, **kwargs):
"""Returns the z-coordinate of a point.
Parameters
----------
point : Geometry or array_like
Non-point geometries or geometries without 3rd dimension will result
in NaN being returned.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_x, get_y
Examples
--------
>>> from shapely import MultiPoint, Point
>>> get_z(Point(1, 2, 3))
3.0
>>> get_z(Point(1, 2))
nan
>>> get_z(MultiPoint([(1, 1, 1), (2, 2, 2)]))
nan
"""
return lib.get_z(point, **kwargs)
# linestrings
@multithreading_enabled
def get_point(geometry, index, **kwargs):
"""Returns the nth point of a linestring or linearring.
Parameters
----------
geometry : Geometry or array_like
index : int or array_like
Negative values count from the end of the linestring backwards.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_num_points
Examples
--------
>>> from shapely import LinearRing, LineString, MultiPoint, Point
>>> line = LineString([(0, 0), (1, 1), (2, 2), (3, 3)])
>>> get_point(line, 1)
<POINT (1 1)>
>>> get_point(line, -2)
<POINT (2 2)>
>>> get_point(line, [0, 3]).tolist()
[<POINT (0 0)>, <POINT (3 3)>]
The functcion works the same for LinearRing input:
>>> get_point(LinearRing([(0, 0), (1, 1), (2, 2), (0, 0)]), 1)
<POINT (1 1)>
For non-linear geometries it returns None:
>>> get_point(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]), 1) is None
True
>>> get_point(Point(1, 1), 0) is None
True
"""
return lib.get_point(geometry, np.intc(index), **kwargs)
@multithreading_enabled
def get_num_points(geometry, **kwargs):
"""Returns number of points in a linestring or linearring.
Returns 0 for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
The number of points in geometries other than linestring or linearring
equals zero.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_point
get_num_geometries
Examples
--------
>>> from shapely import LineString, MultiPoint
>>> get_num_points(LineString([(0, 0), (1, 1), (2, 2), (3, 3)]))
4
>>> get_num_points(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]))
0
>>> get_num_points(None)
0
"""
return lib.get_num_points(geometry, **kwargs)
# polygons
@multithreading_enabled
def get_exterior_ring(geometry, **kwargs):
"""Returns the exterior ring of a polygon.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_interior_ring
Examples
--------
>>> from shapely import Point, Polygon
>>> get_exterior_ring(Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]))
<LINEARRING (0 0, 0 10, 10 10, 10 0, 0 0)>
>>> get_exterior_ring(Point(1, 1)) is None
True
"""
return lib.get_exterior_ring(geometry, **kwargs)
@multithreading_enabled
def get_interior_ring(geometry, index, **kwargs):
"""Returns the nth interior ring of a polygon.
Parameters
----------
geometry : Geometry or array_like
index : int or array_like
Negative values count from the end of the interior rings backwards.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_exterior_ring
get_num_interior_rings
Examples
--------
>>> from shapely import Point, Polygon
>>> polygon_with_hole = Polygon(
... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]]
... )
>>> get_interior_ring(polygon_with_hole, 0)
<LINEARRING (2 2, 2 4, 4 4, 4 2, 2 2)>
>>> get_interior_ring(polygon_with_hole, 1) is None
True
>>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
>>> get_interior_ring(polygon, 0) is None
True
>>> get_interior_ring(Point(0, 0), 0) is None
True
"""
return lib.get_interior_ring(geometry, np.intc(index), **kwargs)
@multithreading_enabled
def get_num_interior_rings(geometry, **kwargs):
"""Returns number of internal rings in a polygon
Returns 0 for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
The number of interior rings in non-polygons equals zero.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_exterior_ring
get_interior_ring
Examples
--------
>>> from shapely import Point, Polygon
>>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
>>> get_num_interior_rings(polygon)
0
>>> polygon_with_hole = Polygon(
... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]]
... )
>>> get_num_interior_rings(polygon_with_hole)
1
>>> get_num_interior_rings(Point(0, 0))
0
>>> get_num_interior_rings(None)
0
"""
return lib.get_num_interior_rings(geometry, **kwargs)
# collections
@multithreading_enabled
def get_geometry(geometry, index, **kwargs):
"""Returns the nth geometry from a collection of geometries.
Parameters
----------
geometry : Geometry or array_like
index : int or array_like
Negative values count from the end of the collection backwards.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Notes
-----
- simple geometries act as length-1 collections
- out-of-range values return None
See also
--------
get_num_geometries, get_parts
Examples
--------
>>> from shapely import Point, MultiPoint
>>> multipoint = MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)])
>>> get_geometry(multipoint, 1)
<POINT (1 1)>
>>> get_geometry(multipoint, -1)
<POINT (3 3)>
>>> get_geometry(multipoint, 5) is None
True
>>> get_geometry(Point(1, 1), 0)
<POINT (1 1)>
>>> get_geometry(Point(1, 1), 1) is None
True
"""
return lib.get_geometry(geometry, np.intc(index), **kwargs)
def get_parts(geometry, return_index=False):
"""Gets parts of each GeometryCollection or Multi* geometry object; returns
a copy of each geometry in the GeometryCollection or Multi* geometry object.
Note: This does not return the individual parts of Multi* geometry objects in
a GeometryCollection. You may need to call this function multiple times to
return individual parts of Multi* geometry objects in a GeometryCollection.
Parameters
----------
geometry : Geometry or array_like
return_index : bool, default False
If True, will return a tuple of ndarrays of (parts, indexes), where indexes
are the indexes of the original geometries in the source array.
Returns
-------
ndarray of parts or tuple of (parts, indexes)
See also
--------
get_geometry, get_rings
Examples
--------
>>> from shapely import MultiPoint
>>> get_parts(MultiPoint([(0, 1), (2, 3)])).tolist()
[<POINT (0 1)>, <POINT (2 3)>]
>>> parts, index = get_parts([MultiPoint([(0, 1)]), MultiPoint([(4, 5), (6, 7)])], \
return_index=True)
>>> parts.tolist()
[<POINT (0 1)>, <POINT (4 5)>, <POINT (6 7)>]
>>> index.tolist()
[0, 1, 1]
"""
geometry = np.asarray(geometry, dtype=np.object_)
geometry = np.atleast_1d(geometry)
if geometry.ndim != 1:
raise ValueError("Array should be one dimensional")
if return_index:
return _geometry_helpers.get_parts(geometry)
return _geometry_helpers.get_parts(geometry)[0]
def get_rings(geometry, return_index=False):
"""Gets rings of Polygon geometry object.
For each Polygon, the first returned ring is always the exterior ring
and potential subsequent rings are interior rings.
If the geometry is not a Polygon, nothing is returned (empty array for
scalar geometry input or no element in output array for array input).
Parameters
----------
geometry : Geometry or array_like
return_index : bool, default False
If True, will return a tuple of ndarrays of (rings, indexes), where
indexes are the indexes of the original geometries in the source array.
Returns
-------
ndarray of rings or tuple of (rings, indexes)
See also
--------
get_exterior_ring, get_interior_ring, get_parts
Examples
--------
>>> from shapely import Polygon
>>> polygon_with_hole = Polygon(
... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]]
... )
>>> get_rings(polygon_with_hole).tolist()
[<LINEARRING (0 0, 0 10, 10 10, 10 0, 0 0)>,
<LINEARRING (2 2, 2 4, 4 4, 4 2, 2 2)>]
With ``return_index=True``:
>>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
>>> rings, index = get_rings([polygon, polygon_with_hole], return_index=True)
>>> rings.tolist()
[<LINEARRING (0 0, 2 0, 2 2, 0 2, 0 0)>,
<LINEARRING (0 0, 0 10, 10 10, 10 0, 0 0)>,
<LINEARRING (2 2, 2 4, 4 4, 4 2, 2 2)>]
>>> index.tolist()
[0, 1, 1]
"""
geometry = np.asarray(geometry, dtype=np.object_)
geometry = np.atleast_1d(geometry)
if geometry.ndim != 1:
raise ValueError("Array should be one dimensional")
if return_index:
return _geometry_helpers.get_parts(geometry, extract_rings=True)
return _geometry_helpers.get_parts(geometry, extract_rings=True)[0]
@multithreading_enabled
def get_num_geometries(geometry, **kwargs):
"""Returns number of geometries in a collection.
Returns 0 for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
The number of geometries in points, linestrings, linearrings and
polygons equals one.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_num_points
get_geometry
Examples
--------
>>> from shapely import MultiPoint, Point
>>> get_num_geometries(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]))
4
>>> get_num_geometries(Point(1, 1))
1
>>> get_num_geometries(None)
0
"""
return lib.get_num_geometries(geometry, **kwargs)
@requires_geos("3.6.0")
@multithreading_enabled
def get_precision(geometry, **kwargs):
"""Get the precision of a geometry.
If a precision has not been previously set, it will be 0 (double
precision). Otherwise, it will return the precision grid size that was
set on a geometry.
Returns NaN for not-a-geometry values.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
set_precision
Examples
--------
>>> from shapely import Point
>>> point = Point(1, 1)
>>> get_precision(point)
0.0
>>> geometry = set_precision(point, 1.0)
>>> get_precision(geometry)
1.0
>>> get_precision(None)
nan
"""
return lib.get_precision(geometry, **kwargs)
class SetPrecisionMode(ParamEnum):
valid_output = 0
pointwise = 1
keep_collapsed = 2
@requires_geos("3.6.0")
@multithreading_enabled
def set_precision(geometry, grid_size, mode="valid_output", **kwargs):
"""Returns geometry with the precision set to a precision grid size.
By default, geometries use double precision coordinates (grid_size = 0).
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`` 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.
Notes:
* 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
----------
geometry : Geometry or array_like
grid_size : float
Precision grid size. If 0, will use double precision (will not modify
geometry if precision grid size was not previously set). If this
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 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.
.. 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.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_precision
Examples
--------
>>> from shapely import LineString, Point
>>> set_precision(Point(0.9, 0.9), 1.0)
<POINT (1 1)>
>>> set_precision(Point(0.9, 0.9, 0.9), 1.0)
<POINT Z (1 1 0.9)>
>>> set_precision(LineString([(0, 0), (0, 0.1), (0, 1), (1, 1)]), 1.0)
<LINESTRING (0 0, 0 1, 1 1)>
>>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="valid_output")
<LINESTRING Z EMPTY>
>>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="pointwise")
<LINESTRING (0 0, 0 0, 0 0)>
>>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="keep_collapsed")
<LINESTRING (0 0, 0 0)>
>>> set_precision(None, 1.0) is None
True
"""
if isinstance(mode, str):
mode = SetPrecisionMode.get_value(mode)
elif not np.isscalar(mode):
raise TypeError("mode only accepts scalar values")
if mode == SetPrecisionMode.pointwise and geos_version < (3, 10, 0):
warnings.warn(
"'pointwise' is only supported for GEOS 3.10",
UserWarning,
stacklevel=2,
)
return lib.set_precision(geometry, grid_size, np.intc(mode), **kwargs)
@multithreading_enabled
def force_2d(geometry, **kwargs):
"""Forces the dimensionality of a geometry to 2D.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point, Polygon, from_wkt
>>> force_2d(Point(0, 0, 1))
<POINT (0 0)>
>>> force_2d(Point(0, 0))
<POINT (0 0)>
>>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)]))
<LINESTRING (0 0, 0 1, 1 1)>
>>> force_2d(from_wkt("POLYGON Z EMPTY"))
<POLYGON EMPTY>
>>> force_2d(None) is None
True
"""
return lib.force_2d(geometry, **kwargs)
@multithreading_enabled
def force_3d(geometry, z=0.0, **kwargs):
"""Forces the dimensionality of a geometry to 3D.
2D geometries will get the provided Z coordinate; Z coordinates of 3D geometries
are unchanged (unless they are nan).
Note that for empty geometries, 3D is only supported since GEOS 3.9 and then
still only for simple geometries (non-collections).
Parameters
----------
geometry : Geometry or array_like
z : float or array_like, default 0.0
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point
>>> force_3d(Point(0, 0), z=3)
<POINT Z (0 0 3)>
>>> force_3d(Point(0, 0, 0), z=3)
<POINT Z (0 0 0)>
>>> force_3d(LineString([(0, 0), (0, 1), (1, 1)]))
<LINESTRING Z (0 0 0, 0 1 0, 1 1 0)>
>>> force_3d(None) is None
True
"""
if np.isnan(z).any():
raise ValueError("It is not allowed to set the Z coordinate to NaN.")
return lib.force_3d(geometry, z, **kwargs)
@@ -0,0 +1,51 @@
"""
Provides a wrapper for GEOS types and functions.
Note: GEOS functions in Cython must be called using the get_geos_handle context manager.
Example:
with get_geos_handle() as geos_handle:
SomeGEOSFunc(geos_handle, ...<other params>)
"""
cdef extern from "geos_c.h":
# Types
ctypedef void *GEOSContextHandle_t
ctypedef struct GEOSGeometry
ctypedef struct GEOSCoordSequence
ctypedef void (*GEOSMessageHandler_r)(const char *message, void *userdata)
# GEOS Context & Messaging
GEOSContextHandle_t GEOS_init_r() nogil
void GEOS_finish_r(GEOSContextHandle_t handle) nogil
void GEOSContext_setErrorMessageHandler_r(GEOSContextHandle_t handle, GEOSMessageHandler_r ef, void* userData) nogil
void GEOSContext_setNoticeMessageHandler_r(GEOSContextHandle_t handle, GEOSMessageHandler_r nf, void* userData) nogil
# Geometry functions
const GEOSGeometry* GEOSGetGeometryN_r(GEOSContextHandle_t handle, const GEOSGeometry* g, int n) nogil
const GEOSGeometry* GEOSGetExteriorRing_r(GEOSContextHandle_t handle, const GEOSGeometry* g) nogil
const GEOSGeometry* GEOSGetInteriorRingN_r(GEOSContextHandle_t handle, const GEOSGeometry* g, int n) nogil
int GEOSGeomTypeId_r(GEOSContextHandle_t handle, GEOSGeometry* g) nogil
# Geometry creation / destruction
GEOSGeometry* GEOSGeom_clone_r(GEOSContextHandle_t handle, const GEOSGeometry* g) nogil
GEOSGeometry* GEOSGeom_createPoint_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil
GEOSGeometry* GEOSGeom_createLineString_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil
GEOSGeometry* GEOSGeom_createLinearRing_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil
GEOSGeometry* GEOSGeom_createEmptyPolygon_r(GEOSContextHandle_t handle) nogil
GEOSGeometry* GEOSGeom_createPolygon_r(GEOSContextHandle_t handle, GEOSGeometry* shell, GEOSGeometry** holes, unsigned int nholes) nogil
GEOSGeometry* GEOSGeom_createCollection_r(GEOSContextHandle_t handle, int type, GEOSGeometry** geoms, unsigned int ngeoms) nogil
void GEOSGeom_destroy_r(GEOSContextHandle_t handle, GEOSGeometry* g) nogil
# Coordinate sequences
GEOSCoordSequence* GEOSCoordSeq_create_r(GEOSContextHandle_t handle, unsigned int size, unsigned int dims) nogil
void GEOSCoordSeq_destroy_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil
int GEOSCoordSeq_setX_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil
int GEOSCoordSeq_setY_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil
int GEOSCoordSeq_setZ_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil
cdef class get_geos_handle:
cdef GEOSContextHandle_t handle
cdef char* last_error
cdef char* last_warning
cdef GEOSContextHandle_t __enter__(self)
@@ -0,0 +1,35 @@
"""
Provides a wrapper for the shapely.lib C API for use in Cython.
Internally, the shapely C extension uses a PyCapsule to provide run-time access
to function pointers within the C API.
To use these functions, you must first call the following function in each Cython module:
`import_shapely_c_api()`
This uses a macro to dynamically load the functions from pointers in the PyCapsule.
Each C function in shapely.lib exposed in the C API must be specially-wrapped to enable
this capability.
Segfaults will occur if the C API is not imported properly.
"""
cimport numpy as np
from cpython.ref cimport PyObject
from shapely._geos cimport GEOSContextHandle_t, GEOSCoordSequence, GEOSGeometry
cdef extern from "c_api.h":
# shapely.lib C API loader; returns -1 on error
# MUST be called before calling other C API functions
int import_shapely_c_api() except -1
# C functions provided by the shapely.lib C API
# Note: GeometryObjects are always managed as Python objects
# in Cython to avoid memory leaks, not PyObject* (even though
# they are declared that way in the header file).
object PyGEOS_CreateGeometry(GEOSGeometry *ptr, GEOSContextHandle_t ctx)
char PyGEOS_GetGEOSGeometry(PyObject *obj, GEOSGeometry **out) nogil
GEOSCoordSequence* PyGEOS_CoordSeq_FromBuffer(GEOSContextHandle_t ctx, const double* buf,
unsigned int size, unsigned int dims,
char ring_closure) nogil
@@ -0,0 +1,453 @@
"""
This modules provides a conversion to / from a ragged (or "jagged") array
representation of the geometries.
A ragged array is an irregular array of arrays of which each element can have
a different length. As a result, such an array cannot be represented as a
standard, rectangular nD array.
The coordinates of geometries can be represented as arrays of arrays of
coordinate pairs (possibly multiple levels of nesting, depending on the
geometry type).
Geometries, as a ragged array of coordinates, can be efficiently represented
as contiguous arrays of coordinates provided that there is another data
structure that keeps track of which range of coordinate values corresponds
to a given geometry. This can be done using offsets, counts, or indices.
This module currently implements offsets into the coordinates array. This
is the ragged array representation defined by the the Apache Arrow project
as "variable size list array" (https://arrow.apache.org/docs/format/Columnar.html#variable-size-list-layout).
See for example https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#representations-features
for different options.
The exact usage of the Arrow list array with varying degrees of nesting for the
different geometry types is defined by the GeoArrow project:
https://github.com/geoarrow/geoarrow
"""
import numpy as np
from shapely import creation
from shapely._geometry import (
GeometryType,
get_coordinate_dimension,
get_parts,
get_rings,
get_type_id,
)
from shapely.coordinates import get_coordinates
from shapely.predicates import is_empty, is_missing
__all__ = ["to_ragged_array", "from_ragged_array"]
# # GEOS -> coords/offset arrays (to_ragged_array)
def _get_arrays_point(arr, include_z):
# only one array of coordinates
coords = get_coordinates(arr, include_z=include_z)
# empty points are represented by NaNs
# + 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))
coords = np.insert(coords, indices, np.nan, axis=0)
return coords, ()
def _indices_to_offsets(indices, n):
offsets = np.insert(np.bincount(indices).cumsum(), 0, 0)
if len(offsets) != n + 1:
# last geometries might be empty or missing
offsets = np.pad(
offsets,
(0, n + 1 - len(offsets)),
"constant",
constant_values=offsets[-1],
)
return offsets
def _get_arrays_multipoint(arr, include_z):
# explode/flatten the MultiPoints
_, part_indices = get_parts(arr, return_index=True)
# the offsets into the multipoint parts
offsets = _indices_to_offsets(part_indices, len(arr))
# only one array of coordinates
coords = get_coordinates(arr, include_z=include_z)
return coords, (offsets,)
def _get_arrays_linestring(arr, include_z):
# the coords and offsets into the coordinates of the linestrings
coords, indices = get_coordinates(arr, return_index=True, include_z=include_z)
offsets = _indices_to_offsets(indices, len(arr))
return coords, (offsets,)
def _get_arrays_multilinestring(arr, include_z):
# explode/flatten the MultiLineStrings
arr_flat, part_indices = get_parts(arr, return_index=True)
# the offsets into the multilinestring parts
offsets2 = _indices_to_offsets(part_indices, len(arr))
# the coords and offsets into the coordinates of the linestrings
coords, indices = get_coordinates(arr_flat, return_index=True, include_z=include_z)
offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0)
return coords, (offsets1, offsets2)
def _get_arrays_polygon(arr, include_z):
# explode/flatten the Polygons into Rings
arr_flat, ring_indices = get_rings(arr, return_index=True)
# the offsets into the exterior/interior rings of the multipolygon parts
offsets2 = _indices_to_offsets(ring_indices, len(arr))
# the coords and offsets into the coordinates of the rings
coords, indices = get_coordinates(arr_flat, return_index=True, include_z=include_z)
offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0)
return coords, (offsets1, offsets2)
def _get_arrays_multipolygon(arr, include_z):
# explode/flatten the MultiPolygons
arr_flat, part_indices = get_parts(arr, return_index=True)
# the offsets into the multipolygon parts
offsets3 = _indices_to_offsets(part_indices, len(arr))
# explode/flatten the Polygons into Rings
arr_flat2, ring_indices = get_rings(arr_flat, return_index=True)
# the offsets into the exterior/interior rings of the multipolygon parts
offsets2 = np.insert(np.bincount(ring_indices).cumsum(), 0, 0)
# the coords and offsets into the coordinates of the rings
coords, indices = get_coordinates(arr_flat2, return_index=True, include_z=include_z)
offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0)
return coords, (offsets1, offsets2, offsets3)
def to_ragged_array(geometries, include_z=None):
"""
Converts geometries to a ragged array representation using a contiguous
array of coordinates and offset arrays.
This function converts an array of geometries to a ragged array
(i.e. irregular array of arrays) of coordinates, represented in memory
using a single contiguous array of the coordinates, and
up to 3 offset arrays that keep track where each sub-array
starts and ends.
This follows the in-memory layout of the variable size list arrays defined
by Apache Arrow, as specified for geometries by the GeoArrow project:
https://github.com/geoarrow/geoarrow.
Parameters
----------
geometries : array_like
Array of geometries (1-dimensional).
include_z : bool, default None
If False, return 2D geometries. If True, include the third dimension
in the output (if a geometry has no third dimension, the z-coordinates
will be NaN). By default, will infer the dimensionality from the
input geometries. Note that this inference can be unreliable with
empty geometries (for a guaranteed result, it is recommended to
specify the keyword).
Returns
-------
tuple of (geometry_type, coords, offsets)
geometry_type : GeometryType
The type of the input geometries (required information for
roundtrip).
coords : np.ndarray
Contiguous array of shape (n, 2) or (n, 3) of all coordinates
of all input geometries.
offsets: tuple of np.ndarray
Offset arrays that make it possible to reconstruct the
geometries from the flat coordinates array. The number of
offset arrays depends on the geometry type. See
https://github.com/geoarrow/geoarrow/blob/main/format.md
for details.
Notes
-----
Mixed singular and multi geometry types of the same basic type are
allowed (e.g., Point and MultiPoint) and all singular types will be
treated as multi types.
GeometryCollections and other mixed geometry types are not supported.
See also
--------
from_ragged_array
Examples
--------
Consider a Polygon with one hole (interior ring):
>>> import shapely
>>> polygon = shapely.Polygon(
... [(0, 0), (10, 0), (10, 10), (0, 10)],
... holes=[[(2, 2), (3, 2), (2, 3)]]
... )
>>> polygon
<POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 3 2, 2 3, 2 2))>
This polygon can be thought of as a list of rings (first ring is the
exterior ring, subsequent rings are the interior rings), and each ring
as a list of coordinate pairs. This is very similar to how GeoJSON
represents the coordinates:
>>> import json
>>> json.loads(shapely.to_geojson(polygon))["coordinates"]
[[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]],
[[2.0, 2.0], [3.0, 2.0], [2.0, 3.0], [2.0, 2.0]]]
This function will return a similar list of lists of lists, but
using a single contiguous array of coordinates, and multiple arrays of
offsets:
>>> geometry_type, coords, offsets = shapely.to_ragged_array([polygon])
>>> geometry_type
<GeometryType.POLYGON: 3>
>>> coords
array([[ 0., 0.],
[10., 0.],
[10., 10.],
[ 0., 10.],
[ 0., 0.],
[ 2., 2.],
[ 3., 2.],
[ 2., 3.],
[ 2., 2.]])
>>> offsets
(array([0, 5, 9]), array([0, 2]))
As an example how to interpret the offsets: the i-th ring in the
coordinates is represented by ``offsets[0][i]`` to ``offsets[0][i+1]``:
>>> exterior_ring_start, exterior_ring_end = offsets[0][0], offsets[0][1]
>>> coords[exterior_ring_start:exterior_ring_end]
array([[ 0., 0.],
[10., 0.],
[10., 10.],
[ 0., 10.],
[ 0., 0.]])
"""
geometries = np.asarray(geometries)
if include_z is None:
include_z = np.any(
get_coordinate_dimension(geometries[~is_empty(geometries)]) == 3
)
geom_types = np.unique(get_type_id(geometries))
# ignore missing values (type of -1)
geom_types = geom_types[geom_types >= 0]
if len(geom_types) == 1:
typ = GeometryType(geom_types[0])
if typ == GeometryType.POINT:
coords, offsets = _get_arrays_point(geometries, include_z)
elif typ == GeometryType.LINESTRING:
coords, offsets = _get_arrays_linestring(geometries, include_z)
elif typ == GeometryType.POLYGON:
coords, offsets = _get_arrays_polygon(geometries, include_z)
elif typ == GeometryType.MULTIPOINT:
coords, offsets = _get_arrays_multipoint(geometries, include_z)
elif typ == GeometryType.MULTILINESTRING:
coords, offsets = _get_arrays_multilinestring(geometries, include_z)
elif typ == GeometryType.MULTIPOLYGON:
coords, offsets = _get_arrays_multipolygon(geometries, include_z)
else:
raise ValueError(f"Geometry type {typ.name} is not supported")
elif len(geom_types) == 2:
if set(geom_types) == {GeometryType.POINT, GeometryType.MULTIPOINT}:
typ = GeometryType.MULTIPOINT
coords, offsets = _get_arrays_multipoint(geometries, include_z)
elif set(geom_types) == {GeometryType.LINESTRING, GeometryType.MULTILINESTRING}:
typ = GeometryType.MULTILINESTRING
coords, offsets = _get_arrays_multilinestring(geometries, include_z)
elif set(geom_types) == {GeometryType.POLYGON, GeometryType.MULTIPOLYGON}:
typ = GeometryType.MULTIPOLYGON
coords, offsets = _get_arrays_multipolygon(geometries, include_z)
else:
raise ValueError(
"Geometry type combination is not supported "
f"({[GeometryType(t).name for t in geom_types]})"
)
else:
raise ValueError(
"Geometry type combination is not supported "
f"({[GeometryType(t).name for t in geom_types]})"
)
return typ, coords, offsets
# # coords/offset arrays -> GEOS (from_ragged_array)
def _point_from_flatcoords(coords):
result = creation.points(coords)
# Older versions of GEOS (<= 3.9) don't automatically convert NaNs
# to empty points -> do manually
empties = np.isnan(coords).all(axis=1)
if empties.any():
result[empties] = creation.empty(1, geom_type=GeometryType.POINT).item()
return result
def _multipoint_from_flatcoords(coords, offsets):
# recreate points
points = creation.points(coords)
# recreate multipoints
multipoint_parts = np.diff(offsets)
multipoint_indices = np.repeat(np.arange(len(multipoint_parts)), multipoint_parts)
result = np.empty(len(offsets) - 1, dtype=object)
result = creation.multipoints(points, indices=multipoint_indices, out=result)
result[multipoint_parts == 0] = creation.empty(
1, geom_type=GeometryType.MULTIPOINT
).item()
return result
def _linestring_from_flatcoords(coords, offsets):
# recreate linestrings
linestring_n = np.diff(offsets)
linestring_indices = np.repeat(np.arange(len(linestring_n)), linestring_n)
result = np.empty(len(offsets) - 1, dtype=object)
result = creation.linestrings(coords, indices=linestring_indices, out=result)
result[linestring_n == 0] = creation.empty(
1, geom_type=GeometryType.LINESTRING
).item()
return result
def _multilinestrings_from_flatcoords(coords, offsets1, offsets2):
# recreate linestrings
linestrings = _linestring_from_flatcoords(coords, offsets1)
# recreate multilinestrings
multilinestring_parts = np.diff(offsets2)
multilinestring_indices = np.repeat(
np.arange(len(multilinestring_parts)), multilinestring_parts
)
result = np.empty(len(offsets2) - 1, dtype=object)
result = creation.multilinestrings(
linestrings, indices=multilinestring_indices, out=result
)
result[multilinestring_parts == 0] = creation.empty(
1, geom_type=GeometryType.MULTILINESTRING
).item()
return result
def _polygon_from_flatcoords(coords, offsets1, offsets2):
# recreate rings
ring_lengths = np.diff(offsets1)
ring_indices = np.repeat(np.arange(len(ring_lengths)), ring_lengths)
rings = creation.linearrings(coords, indices=ring_indices)
# recreate polygons
polygon_rings_n = np.diff(offsets2)
polygon_indices = np.repeat(np.arange(len(polygon_rings_n)), polygon_rings_n)
result = np.empty(len(offsets2) - 1, dtype=object)
result = creation.polygons(rings, indices=polygon_indices, out=result)
result[polygon_rings_n == 0] = creation.empty(
1, geom_type=GeometryType.POLYGON
).item()
return result
def _multipolygons_from_flatcoords(coords, offsets1, offsets2, offsets3):
# recreate polygons
polygons = _polygon_from_flatcoords(coords, offsets1, offsets2)
# recreate multipolygons
multipolygon_parts = np.diff(offsets3)
multipolygon_indices = np.repeat(
np.arange(len(multipolygon_parts)), multipolygon_parts
)
result = np.empty(len(offsets3) - 1, dtype=object)
result = creation.multipolygons(polygons, indices=multipolygon_indices, out=result)
result[multipolygon_parts == 0] = creation.empty(
1, geom_type=GeometryType.MULTIPOLYGON
).item()
return result
def from_ragged_array(geometry_type, coords, offsets=None):
"""
Creates geometries from a contiguous array of coordinates
and offset arrays.
This function creates geometries from the ragged array representation
as returned by ``to_ragged_array``.
This follows the in-memory layout of the variable size list arrays defined
by Apache Arrow, as specified for geometries by the GeoArrow project:
https://github.com/geoarrow/geoarrow.
See :func:`to_ragged_array` for more details.
Parameters
----------
geometry_type : GeometryType
The type of geometry to create.
coords : np.ndarray
Contiguous array of shape (n, 2) or (n, 3) of all coordinates
for the geometries.
offsets: tuple of np.ndarray
Offset arrays that allow to reconstruct the geometries based on the
flat coordinates array. The number of offset arrays depends on the
geometry type. See
https://github.com/geoarrow/geoarrow/blob/main/format.md for details.
Returns
-------
np.ndarray
Array of geometries (1-dimensional).
See Also
--------
to_ragged_array
"""
if geometry_type == GeometryType.POINT:
assert offsets is None or len(offsets) == 0
return _point_from_flatcoords(coords)
if geometry_type == GeometryType.LINESTRING:
return _linestring_from_flatcoords(coords, *offsets)
if geometry_type == GeometryType.POLYGON:
return _polygon_from_flatcoords(coords, *offsets)
elif geometry_type == GeometryType.MULTIPOINT:
return _multipoint_from_flatcoords(coords, *offsets)
elif geometry_type == GeometryType.MULTILINESTRING:
return _multilinestrings_from_flatcoords(coords, *offsets)
elif geometry_type == GeometryType.MULTIPOLYGON:
return _multipolygons_from_flatcoords(coords, *offsets)
else:
raise ValueError(f"Geometry type {geometry_type.name} is not supported")
@@ -0,0 +1,21 @@
# This file was generated by 'versioneer.py' (0.28) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
import json
version_json = '''
{
"date": "2025-01-30T18:00:49-0700",
"dirty": false,
"error": null,
"full-revisionid": "341209eebf905ad10d3050b67ab495129963dae8",
"version": "2.0.7"
}
''' # END VERSION_JSON
def get_versions():
return json.loads(version_json)
@@ -0,0 +1,266 @@
"""Affine transforms, both in general and specific, named transforms."""
from math import cos, pi, sin, tan
import numpy as np
import shapely
__all__ = ["affine_transform", "rotate", "scale", "skew", "translate"]
def affine_transform(geom, matrix):
r"""Return a transformed geometry using an affine transformation matrix.
The coefficient matrix is provided as a list or tuple with 6 or 12 items
for 2D or 3D transformations, respectively.
For 2D affine transformations, the 6 parameter matrix is::
[a, b, d, e, xoff, yoff]
which represents the augmented matrix::
[x'] / a b xoff \ [x]
[y'] = | d e yoff | [y]
[1 ] \ 0 0 1 / [1]
or the equations for the transformed coordinates::
x' = a * x + b * y + xoff
y' = d * x + e * y + yoff
For 3D affine transformations, the 12 parameter matrix is::
[a, b, c, d, e, f, g, h, i, xoff, yoff, zoff]
which represents the augmented matrix::
[x'] / a b c xoff \ [x]
[y'] = | d e f yoff | [y]
[z'] | g h i zoff | [z]
[1 ] \ 0 0 0 1 / [1]
or the equations for the transformed coordinates::
x' = a * x + b * y + c * z + xoff
y' = d * x + e * y + f * z + yoff
z' = g * x + h * y + i * z + zoff
"""
if len(matrix) == 6:
ndim = 2
a, b, d, e, xoff, yoff = matrix
if geom.has_z:
ndim = 3
i = 1.0
c = f = g = h = zoff = 0.0
elif len(matrix) == 12:
ndim = 3
a, b, c, d, e, f, g, h, i, xoff, yoff, zoff = matrix
if not geom.has_z:
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)
def _affine_coords(coords):
# 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)
def interpret_origin(geom, origin, ndim):
"""Returns interpreted coordinate tuple for origin parameter.
This is a helper function for other transform functions.
The point of origin can be a keyword 'center' for the 2D bounding box
center, 'centroid' for the geometry's 2D centroid, a Point object or a
coordinate tuple (x0, y0, z0).
"""
# get coordinate tuple from 'origin' from keyword or Point type
if origin == "center":
# bounding box center
minx, miny, maxx, maxy = geom.bounds
origin = ((maxx + minx) / 2.0, (maxy + miny) / 2.0)
elif origin == "centroid":
origin = geom.centroid.coords[0]
elif isinstance(origin, str):
raise ValueError(f"'origin' keyword {origin!r} is not recognized")
elif getattr(origin, "geom_type", None) == "Point":
origin = origin.coords[0]
# origin should now be tuple-like
if len(origin) not in (2, 3):
raise ValueError("Expected number of items in 'origin' to be " "either 2 or 3")
if ndim == 2:
return origin[0:2]
else: # 3D coordinate
if len(origin) == 2:
return origin + (0.0,)
else:
return origin
def rotate(geom, angle, origin="center", use_radians=False):
r"""Returns a rotated geometry on a 2D plane.
The angle of rotation can be specified in either degrees (default) or
radians by setting ``use_radians=True``. Positive angles are
counter-clockwise and negative are clockwise rotations.
The point of origin can be a keyword 'center' for the bounding box
center (default), 'centroid' for the geometry's centroid, a Point object
or a coordinate tuple (x0, y0).
The affine transformation matrix for 2D rotation is:
/ cos(r) -sin(r) xoff \
| sin(r) cos(r) yoff |
\ 0 0 1 /
where the offsets are calculated from the origin Point(x0, y0):
xoff = x0 - x0 * cos(r) + y0 * sin(r)
yoff = y0 - x0 * sin(r) - y0 * cos(r)
"""
if geom.is_empty:
return geom
if not use_radians: # convert from degrees
angle = angle * pi / 180.0
cosp = cos(angle)
sinp = sin(angle)
if abs(cosp) < 2.5e-16:
cosp = 0.0
if abs(sinp) < 2.5e-16:
sinp = 0.0
x0, y0 = interpret_origin(geom, origin, 2)
# fmt: off
matrix = (cosp, -sinp, 0.0,
sinp, cosp, 0.0,
0.0, 0.0, 1.0,
x0 - x0 * cosp + y0 * sinp, y0 - x0 * sinp - y0 * cosp, 0.0)
# fmt: on
return affine_transform(geom, matrix)
def scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin="center"):
r"""Returns a scaled geometry, scaled by factors along each dimension.
The point of origin can be a keyword 'center' for the 2D bounding box
center (default), 'centroid' for the geometry's 2D centroid, a Point
object or a coordinate tuple (x0, y0, z0).
Negative scale factors will mirror or reflect coordinates.
The general 3D affine transformation matrix for scaling is:
/ xfact 0 0 xoff \
| 0 yfact 0 yoff |
| 0 0 zfact zoff |
\ 0 0 0 1 /
where the offsets are calculated from the origin Point(x0, y0, z0):
xoff = x0 - x0 * xfact
yoff = y0 - y0 * yfact
zoff = z0 - z0 * zfact
"""
if geom.is_empty:
return geom
x0, y0, z0 = interpret_origin(geom, origin, 3)
# fmt: off
matrix = (xfact, 0.0, 0.0,
0.0, yfact, 0.0,
0.0, 0.0, zfact,
x0 - x0 * xfact, y0 - y0 * yfact, z0 - z0 * zfact)
# fmt: on
return affine_transform(geom, matrix)
def skew(geom, xs=0.0, ys=0.0, origin="center", use_radians=False):
r"""Returns a skewed geometry, sheared by angles along x and y dimensions.
The shear angle can be specified in either degrees (default) or radians
by setting ``use_radians=True``.
The point of origin can be a keyword 'center' for the bounding box
center (default), 'centroid' for the geometry's centroid, a Point object
or a coordinate tuple (x0, y0).
The general 2D affine transformation matrix for skewing is:
/ 1 tan(xs) xoff \
| tan(ys) 1 yoff |
\ 0 0 1 /
where the offsets are calculated from the origin Point(x0, y0):
xoff = -y0 * tan(xs)
yoff = -x0 * tan(ys)
"""
if geom.is_empty:
return geom
if not use_radians: # convert from degrees
xs = xs * pi / 180.0
ys = ys * pi / 180.0
tanx = tan(xs)
tany = tan(ys)
if abs(tanx) < 2.5e-16:
tanx = 0.0
if abs(tany) < 2.5e-16:
tany = 0.0
x0, y0 = interpret_origin(geom, origin, 2)
# fmt: off
matrix = (1.0, tanx, 0.0,
tany, 1.0, 0.0,
0.0, 0.0, 1.0,
-y0 * tanx, -x0 * tany, 0.0)
# fmt: on
return affine_transform(geom, matrix)
def translate(geom, xoff=0.0, yoff=0.0, zoff=0.0):
r"""Returns a translated geometry shifted by offsets along each dimension.
The general 3D affine transformation matrix for translation is:
/ 1 0 0 xoff \
| 0 1 0 yoff |
| 0 0 1 zoff |
\ 0 0 0 1 /
"""
if geom.is_empty:
return geom
# fmt: off
matrix = (1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
xoff, yoff, zoff)
# fmt: on
return affine_transform(geom, matrix)
@@ -0,0 +1,58 @@
import math
from itertools import islice
import numpy as np
import shapely
from shapely.affinity import affine_transform
def _oriented_envelope_min_area(geometry, **kwargs):
"""
Computes the oriented envelope (minimum rotated rectangle) that encloses
an input geometry.
This is a fallback implementation for GEOS < 3.12 to have the correct
minimum area behaviour.
"""
if geometry is None:
return None
if geometry.is_empty:
return shapely.from_wkt("POLYGON EMPTY")
# first compute the convex hull
hull = geometry.convex_hull
try:
coords = hull.exterior.coords
except AttributeError: # may be a Point or a LineString
return hull
# generate the edge vectors between the convex hull's coords
edges = (
(pt2[0] - pt1[0], pt2[1] - pt1[1])
for pt1, pt2 in zip(coords, islice(coords, 1, None))
)
def _transformed_rects():
for dx, dy in edges:
# compute the normalized direction vector of the edge
# vector.
length = math.sqrt(dx**2 + dy**2)
ux, uy = dx / length, dy / length
# compute the normalized perpendicular vector
vx, vy = -uy, ux
# transform hull from the original coordinate system to
# the coordinate system defined by the edge and compute
# the axes-parallel bounding rectangle.
transf_rect = affine_transform(hull, (ux, uy, vx, vy, 0, 0)).envelope
# yield the transformed rectangle and a matrix to
# transform it back to the original coordinate system.
yield (transf_rect, (ux, vx, uy, vy, 0, 0))
# 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
)
@@ -0,0 +1,24 @@
import numpy as np
import shapely
def signed_area(ring):
"""Return the signed area enclosed by a ring in linear time using the
algorithm at: https://web.archive.org/web/20080209143651/http://cgafaq.info:80/wiki/Polygon_Area
"""
coords = np.array(ring.coords)[:, :2]
xs, ys = np.vstack([coords, coords[1]]).T
return np.sum(xs[1:-1] * (ys[2:] - ys[:-2])) / 2.0
def is_ccw_impl(name=None):
"""Predicate implementation"""
def is_ccw_op(ring):
return signed_area(ring) >= 0.0
if shapely.geos_version >= (3, 7, 0):
return shapely.is_ccw
else:
return is_ccw_op
@@ -0,0 +1,139 @@
from heapq import heappop, heappush
from shapely.errors import TopologicalError
from shapely.geometry import Point
class Cell:
"""A `Cell`'s centroid property is a potential solution to finding the pole
of inaccessibility for a given polygon. Rich comparison operators are used
for sorting `Cell` objects in a priority queue based on the potential
maximum distance of any theoretical point within a cell to a given
polygon's exterior boundary.
"""
def __init__(self, x, y, h, polygon):
self.x = x
self.y = y
self.h = h # half of cell size
self.centroid = Point(x, y) # cell centroid, potential solution
# distance from cell centroid to polygon exterior
self.distance = self._dist(polygon)
# max distance to polygon exterior within a cell
self.max_distance = self.distance + h * 1.4142135623730951 # sqrt(2)
# rich comparison operators for sorting in minimum priority queue
def __lt__(self, other):
return self.max_distance > other.max_distance
def __le__(self, other):
return self.max_distance >= other.max_distance
def __eq__(self, other):
return self.max_distance == other.max_distance
def __ne__(self, other):
return self.max_distance != other.max_distance
def __gt__(self, other):
return self.max_distance < other.max_distance
def __ge__(self, other):
return self.max_distance <= other.max_distance
def _dist(self, polygon):
"""Signed distance from Cell centroid to polygon outline. The returned
value is negative if the point is outside of the polygon exterior
boundary.
"""
inside = polygon.contains(self.centroid)
distance = self.centroid.distance(polygon.exterior)
for interior in polygon.interiors:
distance = min(distance, self.centroid.distance(interior))
if inside:
return distance
return -distance
def polylabel(polygon, tolerance=1.0):
"""Finds pole of inaccessibility for a given polygon. Based on
Vladimir Agafonkin's https://github.com/mapbox/polylabel
Parameters
----------
polygon : shapely.geometry.Polygon
tolerance : int or float, optional
`tolerance` represents the highest resolution in units of the
input geometry that will be considered for a solution. (default
value is 1.0).
Returns
-------
shapely.geometry.Point
A point representing the pole of inaccessibility for the given input
polygon.
Raises
------
shapely.errors.TopologicalError
If the input polygon is not a valid geometry.
Example
-------
>>> from shapely import LineString
>>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50),
... (-100, -20), (-150, -200)]).buffer(100)
>>> polylabel(polygon, tolerance=10).wkt
'POINT (59.35615556364569 121.83919629746435)'
"""
if not polygon.is_valid:
raise TopologicalError("Invalid polygon")
minx, miny, maxx, maxy = polygon.bounds
width = maxx - minx
height = maxy - miny
cell_size = min(width, height)
h = cell_size / 2.0
cell_queue = []
# First best cell approximation is one constructed from the centroid
# of the polygon
x, y = polygon.centroid.coords[0]
best_cell = Cell(x, y, 0, polygon)
# Special case for rectangular polygons avoiding floating point error
bbox_cell = Cell(minx + width / 2.0, miny + height / 2, 0, polygon)
if bbox_cell.distance > best_cell.distance:
best_cell = bbox_cell
# build a regular square grid covering the polygon
x = minx
while x < maxx:
y = miny
while y < maxy:
heappush(cell_queue, Cell(x + h, y + h, h, polygon))
y += cell_size
x += cell_size
# minimum priority queue
while cell_queue:
cell = heappop(cell_queue)
# update the best cell if we find a better one
if cell.distance > best_cell.distance:
best_cell = cell
# continue to the next iteration if we can't find a better solution
# based on tolerance
if cell.max_distance - best_cell.distance <= tolerance:
continue
# split the cell into quadrants
h = cell.h / 2.0
heappush(cell_queue, Cell(cell.x - h, cell.y - h, h, polygon))
heappush(cell_queue, Cell(cell.x + h, cell.y - h, h, polygon))
heappush(cell_queue, Cell(cell.x - h, cell.y + h, h, polygon))
heappush(cell_queue, Cell(cell.x + h, cell.y + h, h, polygon))
return best_cell.centroid
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
import numpy as np
from shapely import lib
__all__ = ["transform", "count_coordinates", "get_coordinates", "set_coordinates"]
def transform(geometry, transformation, include_z=False):
"""Returns a copy of a geometry array with a function applied to its
coordinates.
With the default of ``include_z=False``, all returned geometries will be
two-dimensional; the third dimension will be discarded, if present.
When specifying ``include_z=True``, the returned geometries preserve
the dimensionality of the respective input geometries.
Parameters
----------
geometry : Geometry or array_like
transformation : function
A function that transforms a (N, 2) or (N, 3) ndarray of float64 to
another (N, 2) or (N, 3) ndarray of float64.
include_z : bool, default False
If True, include the third dimension in the coordinates array
that is passed to the ``transformation`` function. If a
geometry has no third dimension, the z-coordinates passed to the
function will be NaN.
Examples
--------
>>> from shapely import LineString, Point
>>> transform(Point(0, 0), lambda x: x + 1)
<POINT (1 1)>
>>> transform(LineString([(2, 2), (4, 4)]), lambda x: x * [2, 3])
<LINESTRING (4 6, 8 12)>
>>> transform(None, lambda x: x) is None
True
>>> transform([Point(0, 0), None], lambda x: x).tolist()
[<POINT (0 0)>, None]
By default, the third dimension is ignored:
>>> transform(Point(0, 0, 0), lambda x: x + 1)
<POINT (1 1)>
>>> transform(Point(0, 0, 0), lambda x: x + 1, include_z=True)
<POINT Z (1 1 1)>
"""
geometry_arr = np.array(geometry, dtype=np.object_) # makes a copy
coordinates = lib.get_coordinates(geometry_arr, include_z, False)
new_coordinates = transformation(coordinates)
# check the array to yield understandable error messages
if not isinstance(new_coordinates, np.ndarray):
raise ValueError("The provided transformation did not return a numpy array")
if new_coordinates.dtype != np.float64:
raise ValueError(
"The provided transformation returned an array with an unexpected "
f"dtype ({new_coordinates.dtype})"
)
if new_coordinates.shape != coordinates.shape:
# if the shape is too small we will get a segfault
raise ValueError(
"The provided transformation returned an array with an unexpected "
f"shape ({new_coordinates.shape})"
)
geometry_arr = lib.set_coordinates(geometry_arr, new_coordinates)
if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray):
return geometry_arr.item()
return geometry_arr
def count_coordinates(geometry):
"""Counts the number of coordinate pairs in a geometry array.
Parameters
----------
geometry : Geometry or array_like
Examples
--------
>>> from shapely import LineString, Point
>>> count_coordinates(Point(0, 0))
1
>>> count_coordinates(LineString([(2, 2), (4, 2)]))
2
>>> count_coordinates(None)
0
>>> count_coordinates([Point(0, 0), None])
1
"""
return lib.count_coordinates(np.asarray(geometry, dtype=np.object_))
def get_coordinates(geometry, include_z=False, return_index=False):
"""Gets coordinates from a geometry array as an array of floats.
The shape of the returned array is (N, 2), with N being the number of
coordinate pairs. With the default of ``include_z=False``, three-dimensional
data is ignored. When specifying ``include_z=True``, the shape of the
returned array is (N, 3).
Parameters
----------
geometry : Geometry or array_like
include_z : bool, default False
If, True include the third dimension in the output. If a geometry
has no third dimension, the z-coordinates will be NaN.
return_index : bool, default False
If True, also return the index of each returned geometry as a separate
ndarray of integers. For multidimensional arrays, this indexes into the
flattened array (in C contiguous order).
Examples
--------
>>> from shapely import LineString, Point
>>> get_coordinates(Point(0, 0)).tolist()
[[0.0, 0.0]]
>>> get_coordinates(LineString([(2, 2), (4, 4)])).tolist()
[[2.0, 2.0], [4.0, 4.0]]
>>> get_coordinates(None)
array([], shape=(0, 2), dtype=float64)
By default the third dimension is ignored:
>>> get_coordinates(Point(0, 0, 0)).tolist()
[[0.0, 0.0]]
>>> get_coordinates(Point(0, 0, 0), include_z=True).tolist()
[[0.0, 0.0, 0.0]]
When return_index=True, indexes are returned also:
>>> geometries = [LineString([(2, 2), (4, 4)]), Point(0, 0)]
>>> coordinates, index = get_coordinates(geometries, return_index=True)
>>> coordinates.tolist(), index.tolist()
([[2.0, 2.0], [4.0, 4.0], [0.0, 0.0]], [0, 0, 1])
"""
return lib.get_coordinates(
np.asarray(geometry, dtype=np.object_), include_z, return_index
)
def set_coordinates(geometry, coordinates):
"""Adapts the coordinates of a geometry array in-place.
If the coordinates array has shape (N, 2), all returned geometries
will be two-dimensional, and the third dimension will be discarded,
if present. If the coordinates array has shape (N, 3), the returned
geometries preserve the dimensionality of the input geometries.
.. warning::
The geometry array is modified in-place! If you do not want to
modify the original array, you can do
``set_coordinates(arr.copy(), newcoords)``.
Parameters
----------
geometry : Geometry or array_like
coordinates: array_like
See Also
--------
transform : Returns a copy of a geometry array with a function applied to its
coordinates.
Examples
--------
>>> from shapely import LineString, Point
>>> set_coordinates(Point(0, 0), [[1, 1]])
<POINT (1 1)>
>>> set_coordinates([Point(0, 0), LineString([(0, 0), (0, 0)])], [[1, 2], [3, 4], [5, 6]]).tolist()
[<POINT (1 2)>, <LINESTRING (3 4, 5 6)>]
>>> set_coordinates([None, Point(0, 0)], [[1, 2]]).tolist()
[None, <POINT (1 2)>]
Third dimension of input geometry is discarded if coordinates array does
not include one:
>>> set_coordinates(Point(0, 0, 0), [[1, 1]])
<POINT (1 1)>
>>> set_coordinates(Point(0, 0, 0), [[1, 1, 1]])
<POINT Z (1 1 1)>
"""
geometry_arr = np.asarray(geometry, dtype=np.object_)
coordinates = np.atleast_2d(np.asarray(coordinates)).astype(np.float64)
if coordinates.ndim != 2:
raise ValueError(
"The coordinate array should have dimension of 2 "
f"(has {coordinates.ndim})"
)
n_coords = lib.count_coordinates(geometry_arr)
if (coordinates.shape[0] != n_coords) or (coordinates.shape[1] not in {2, 3}):
raise ValueError(
f"The coordinate array has an invalid shape {coordinates.shape}"
)
lib.set_coordinates(geometry_arr, coordinates)
if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray):
return geometry_arr.item()
return geometry_arr
@@ -0,0 +1,67 @@
"""Coordinate sequence utilities
"""
from array import array
class CoordinateSequence:
"""
Iterative access to coordinate tuples from the parent geometry's coordinate
sequence.
Example:
>>> from shapely.wkt import loads
>>> g = loads('POINT (0.0 0.0)')
>>> list(g.coords)
[(0.0, 0.0)]
"""
def __init__(self, coords):
self._coords = coords
def __len__(self):
return self._coords.shape[0]
def __iter__(self):
for i in range(self.__len__()):
yield tuple(self._coords[i].tolist())
def __getitem__(self, key):
m = self.__len__()
if isinstance(key, int):
if key + m < 0 or key >= m:
raise IndexError("index out of range")
if key < 0:
i = m + key
else:
i = key
return tuple(self._coords[i].tolist())
elif isinstance(key, slice):
res = []
start, stop, stride = key.indices(m)
for i in range(start, stop, stride):
res.append(tuple(self._coords[i].tolist()))
return res
else:
raise TypeError("key must be an index or slice")
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):
"""X and Y arrays"""
m = self.__len__()
x = array("d")
y = array("d")
for i in range(m):
xy = self._coords[i].tolist()
x.append(xy[0])
y.append(xy[1])
return x, y
@@ -0,0 +1,555 @@
import numpy as np
from shapely import Geometry, GeometryType, lib
from shapely._geometry_helpers import collections_1d, simple_geometries_1d
from shapely.decorators import multithreading_enabled
from shapely.io import from_wkt
__all__ = [
"points",
"linestrings",
"linearrings",
"polygons",
"multipoints",
"multilinestrings",
"multipolygons",
"geometrycollections",
"box",
"prepare",
"destroy_prepared",
"empty",
]
def _xyz_to_coords(x, y, z):
if y is None:
return x
if z is None:
coords = np.broadcast_arrays(x, y)
else:
coords = np.broadcast_arrays(x, y, z)
return np.stack(coords, axis=-1)
@multithreading_enabled
def points(coords, y=None, z=None, indices=None, out=None, **kwargs):
"""Create an array of points.
Parameters
----------
coords : array_like
An array of coordinate tuples (2- or 3-dimensional) or, if ``y`` is
provided, an array of x coordinates.
y : array_like, optional
z : array_like, optional
indices : array_like, optional
Indices into the target array where input coordinates belong. If
provided, the coords should be 2D with shape (N, 2) or (N, 3) and
indices should be an array of shape (N,) with integers in increasing
order. Missing indices result in a ValueError unless ``out`` is
provided, in which case the original value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
Examples
--------
>>> points([[0, 1], [4, 5]]).tolist()
[<POINT (0 1)>, <POINT (4 5)>]
>>> points([0, 1, 2])
<POINT Z (0 1 2)>
Notes
-----
- GEOS 3.10, 3.11 and 3.12 automatically converts POINT (nan nan) to POINT EMPTY.
- GEOS 3.10 and 3.11 will transform a 3D point to 2D if its Z coordinate is NaN.
- Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``.
Instead provide the coordinates as an array with shape ``(..., 2)`` or ``(..., 3)`` using only the ``coords`` argument.
"""
coords = _xyz_to_coords(coords, y, z)
if indices is None:
return lib.points(coords, out=out, **kwargs)
else:
return simple_geometries_1d(coords, indices, GeometryType.POINT, out=out)
@multithreading_enabled
def linestrings(coords, y=None, z=None, indices=None, out=None, **kwargs):
"""Create an array of linestrings.
This function will raise an exception if a linestring contains less than
two points.
Parameters
----------
coords : array_like
An array of lists of coordinate tuples (2- or 3-dimensional) or, if ``y``
is provided, an array of lists of x coordinates
y : array_like, optional
z : array_like, optional
indices : array_like, optional
Indices into the target array where input coordinates belong. If
provided, the coords should be 2D with shape (N, 2) or (N, 3) and
indices should be an array of shape (N,) with integers in increasing
order. Missing indices result in a ValueError unless ``out`` is
provided, in which case the original value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
Examples
--------
>>> linestrings([[[0, 1], [4, 5]], [[2, 3], [5, 6]]]).tolist()
[<LINESTRING (0 1, 4 5)>, <LINESTRING (2 3, 5 6)>]
>>> linestrings([[0, 1], [4, 5], [2, 3], [5, 6], [7, 8]], indices=[0, 0, 1, 1, 1]).tolist()
[<LINESTRING (0 1, 4 5)>, <LINESTRING (2 3, 5 6, 7 8)>]
Notes
-----
- Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``.
Instead provide the coordinates as a ``(..., 2)`` or ``(..., 3)`` array using only ``coords``.
"""
coords = _xyz_to_coords(coords, y, z)
if indices is None:
return lib.linestrings(coords, out=out, **kwargs)
else:
return simple_geometries_1d(coords, indices, GeometryType.LINESTRING, out=out)
@multithreading_enabled
def linearrings(coords, y=None, z=None, indices=None, out=None, **kwargs):
"""Create an array of linearrings.
If the provided coords do not constitute a closed linestring, or if there
are only 3 provided coords, the first
coordinate is duplicated at the end to close the ring. This function will
raise an exception if a linearring contains less than three points or if
the terminal coordinates contain NaN (not-a-number).
Parameters
----------
coords : array_like
An array of lists of coordinate tuples (2- or 3-dimensional) or, if ``y``
is provided, an array of lists of x coordinates
y : array_like, optional
z : array_like, optional
indices : array_like, optional
Indices into the target array where input coordinates belong. If
provided, the coords should be 2D with shape (N, 2) or (N, 3) and
indices should be an array of shape (N,) with integers in increasing
order. Missing indices result in a ValueError unless ``out`` is
provided, in which case the original value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
See also
--------
linestrings
Examples
--------
>>> linearrings([[0, 0], [0, 1], [1, 1], [0, 0]])
<LINEARRING (0 0, 0 1, 1 1, 0 0)>
>>> linearrings([[0, 0], [0, 1], [1, 1]])
<LINEARRING (0 0, 0 1, 1 1, 0 0)>
Notes
-----
- Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``.
Instead provide the coordinates as a ``(..., 2)`` or ``(..., 3)`` array using only ``coords``.
"""
coords = _xyz_to_coords(coords, y, z)
if indices is None:
return lib.linearrings(coords, out=out, **kwargs)
else:
return simple_geometries_1d(coords, indices, GeometryType.LINEARRING, out=out)
@multithreading_enabled
def polygons(geometries, holes=None, indices=None, out=None, **kwargs):
"""Create an array of polygons.
Parameters
----------
geometries : array_like
An array of linearrings or coordinates (see linearrings).
Unless ``indices`` are given (see description below), this
include the outer shells only. The ``holes`` argument should be used
to create polygons with holes.
holes : array_like, optional
An array of lists of linearrings that constitute holes for each shell.
Not to be used in combination with ``indices``.
indices : array_like, optional
Indices into the target array where input geometries belong. If
provided, the holes are expected to be present inside ``geometries``;
the first geometry for each index is the outer shell
and all subsequent geometries in that index are the holes.
Both geometries and indices should be 1D and have matching sizes.
Indices should be in increasing order. Missing indices result in a ValueError
unless ``out`` is provided, in which case the original value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
Examples
--------
Polygons are constructed from rings:
>>> ring_1 = linearrings([[0, 0], [0, 10], [10, 10], [10, 0]])
>>> ring_2 = linearrings([[2, 6], [2, 7], [3, 7], [3, 6]])
>>> polygons([ring_1, ring_2])[0]
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))>
>>> polygons([ring_1, ring_2])[1]
<POLYGON ((2 6, 2 7, 3 7, 3 6, 2 6))>
Or from coordinates directly:
>>> polygons([[0, 0], [0, 10], [10, 10], [10, 0]])
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))>
Adding holes can be done using the ``holes`` keyword argument:
>>> polygons(ring_1, holes=[ring_2])
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (2 6, 2 7, 3 7, 3 6, 2 6))>
Or using the ``indices`` argument:
>>> polygons([ring_1, ring_2], indices=[0, 1])[0]
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))>
>>> polygons([ring_1, ring_2], indices=[0, 1])[1]
<POLYGON ((2 6, 2 7, 3 7, 3 6, 2 6))>
>>> polygons([ring_1, ring_2], indices=[0, 0])[0]
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (2 6, 2 7, 3 7, 3 6, 2 6))>
Missing input values (``None``) are ignored and may result in an
empty polygon:
>>> polygons(None)
<POLYGON EMPTY>
>>> polygons(ring_1, holes=[None])
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))>
>>> polygons([ring_1, None], indices=[0, 0])[0]
<POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))>
"""
geometries = np.asarray(geometries)
if not isinstance(geometries, Geometry) and np.issubdtype(
geometries.dtype, np.number
):
geometries = linearrings(geometries)
if indices is not None:
if holes is not None:
raise TypeError("Cannot specify separate holes array when using indices.")
return collections_1d(geometries, indices, GeometryType.POLYGON, out=out)
if holes is None:
# no holes provided: initialize an empty holes array matching shells
shape = geometries.shape + (0,) if isinstance(geometries, np.ndarray) else (0,)
holes = np.empty(shape, dtype=object)
else:
holes = np.asarray(holes)
# convert holes coordinates into linearrings
if np.issubdtype(holes.dtype, np.number):
holes = linearrings(holes)
return lib.polygons(geometries, holes, out=out, **kwargs)
@multithreading_enabled
def box(xmin, ymin, xmax, ymax, ccw=True, **kwargs):
"""Create box polygons.
Parameters
----------
xmin : array_like
ymin : array_like
xmax : array_like
ymax : array_like
ccw : bool, default True
If True, box will be created in counterclockwise direction starting
from bottom right coordinate (xmax, ymin).
If False, box will be created in clockwise direction starting from
bottom left coordinate (xmin, ymin).
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> box(0, 0, 1, 1)
<POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0))>
>>> box(0, 0, 1, 1, ccw=False)
<POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))>
"""
return lib.box(xmin, ymin, xmax, ymax, ccw, **kwargs)
@multithreading_enabled
def multipoints(geometries, indices=None, out=None, **kwargs):
"""Create multipoints from arrays of points
Parameters
----------
geometries : array_like
An array of points or coordinates (see points).
indices : array_like, optional
Indices into the target array where input geometries belong. If
provided, both geometries and indices should be 1D and have matching
sizes. Indices should be in increasing order. Missing indices result
in a ValueError unless ``out`` is provided, in which case the original
value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
Examples
--------
Multipoints are constructed from points:
>>> point_1 = points([1, 1])
>>> point_2 = points([2, 2])
>>> multipoints([point_1, point_2])
<MULTIPOINT (1 1, 2 2)>
>>> multipoints([[point_1, point_2], [point_2, None]]).tolist()
[<MULTIPOINT (1 1, 2 2)>, <MULTIPOINT (2 2)>]
Or from coordinates directly:
>>> multipoints([[0, 0], [2, 2], [3, 3]])
<MULTIPOINT (0 0, 2 2, 3 3)>
Multiple multipoints of different sizes can be constructed efficiently using the
``indices`` keyword argument:
>>> multipoints([point_1, point_2, point_2], indices=[0, 0, 1]).tolist()
[<MULTIPOINT (1 1, 2 2)>, <MULTIPOINT (2 2)>]
Missing input values (``None``) are ignored and may result in an
empty multipoint:
>>> multipoints([None])
<MULTIPOINT EMPTY>
>>> multipoints([point_1, None], indices=[0, 0]).tolist()
[<MULTIPOINT (1 1)>]
>>> multipoints([point_1, None], indices=[0, 1]).tolist()
[<MULTIPOINT (1 1)>, <MULTIPOINT EMPTY>]
"""
typ = GeometryType.MULTIPOINT
geometries = np.asarray(geometries)
if not isinstance(geometries, Geometry) and np.issubdtype(
geometries.dtype, np.number
):
geometries = points(geometries)
if indices is None:
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@multithreading_enabled
def multilinestrings(geometries, indices=None, out=None, **kwargs):
"""Create multilinestrings from arrays of linestrings
Parameters
----------
geometries : array_like
An array of linestrings or coordinates (see linestrings).
indices : array_like, optional
Indices into the target array where input geometries belong. If
provided, both geometries and indices should be 1D and have matching
sizes. Indices should be in increasing order. Missing indices result
in a ValueError unless ``out`` is provided, in which case the original
value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
See also
--------
multipoints
"""
typ = GeometryType.MULTILINESTRING
geometries = np.asarray(geometries)
if not isinstance(geometries, Geometry) and np.issubdtype(
geometries.dtype, np.number
):
geometries = linestrings(geometries)
if indices is None:
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@multithreading_enabled
def multipolygons(geometries, indices=None, out=None, **kwargs):
"""Create multipolygons from arrays of polygons
Parameters
----------
geometries : array_like
An array of polygons or coordinates (see polygons).
indices : array_like, optional
Indices into the target array where input geometries belong. If
provided, both geometries and indices should be 1D and have matching
sizes. Indices should be in increasing order. Missing indices result
in a ValueError unless ``out`` is provided, in which case the original
value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
See also
--------
multipoints
"""
typ = GeometryType.MULTIPOLYGON
geometries = np.asarray(geometries)
if not isinstance(geometries, Geometry) and np.issubdtype(
geometries.dtype, np.number
):
geometries = polygons(geometries)
if indices is None:
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
@multithreading_enabled
def geometrycollections(geometries, indices=None, out=None, **kwargs):
"""Create geometrycollections from arrays of geometries
Parameters
----------
geometries : array_like
An array of geometries
indices : array_like, optional
Indices into the target array where input geometries belong. If
provided, both geometries and indices should be 1D and have matching
sizes. Indices should be in increasing order. Missing indices result
in a ValueError unless ``out`` is provided, in which case the original
value in ``out`` is kept.
out : ndarray, optional
An array (with dtype object) to output the geometries into.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Ignored if ``indices`` is provided.
See also
--------
multipoints
"""
typ = GeometryType.GEOMETRYCOLLECTION
if indices is None:
return lib.create_collection(geometries, np.intc(typ), out=out, **kwargs)
else:
return collections_1d(geometries, indices, typ, out=out)
def prepare(geometry, **kwargs):
"""Prepare a geometry, improving performance of other operations.
A prepared geometry is a normal geometry with added information such as an
index on the line segments. This improves the performance of the following operations:
contains, contains_properly, covered_by, covers, crosses, disjoint, intersects,
overlaps, touches, and within.
Note that if a prepared geometry is modified, the newly created Geometry object is
not prepared. In that case, ``prepare`` should be called again.
This function does not recompute previously prepared geometries;
it is efficient to call this function on an array that partially contains prepared geometries.
This function does not return any values; geometries are modified in place.
Parameters
----------
geometry : Geometry or array_like
Geometries are changed in place
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
is_prepared : Identify whether a geometry is prepared already.
destroy_prepared : Destroy the prepared part of a geometry.
Examples
--------
>>> from shapely import Point, buffer, prepare, contains_properly
>>> poly = buffer(Point(1.0, 1.0), 1)
>>> prepare(poly)
>>> contains_properly(poly, [Point(0.0, 0.0), Point(0.5, 0.5)]).tolist()
[False, True]
"""
lib.prepare(geometry, **kwargs)
def destroy_prepared(geometry, **kwargs):
"""Destroy the prepared part of a geometry, freeing up memory.
Note that the prepared geometry will always be cleaned up if the geometry itself
is dereferenced. This function needs only be called in very specific circumstances,
such as freeing up memory without losing the geometries, or benchmarking.
Parameters
----------
geometry : Geometry or array_like
Geometries are changed in-place
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
prepare
"""
lib.destroy_prepared(geometry, **kwargs)
def empty(shape, geom_type=None, order="C"):
"""Create a geometry array prefilled with None or with empty geometries.
Parameters
----------
shape : int or tuple of int
Shape of the empty array, e.g., ``(2, 3)`` or ``2``.
geom_type : shapely.GeometryType, optional
The desired geometry type in case the array should be prefilled
with empty geometries. Default ``None``.
order : {'C', 'F'}, optional, default: 'C'
Whether to store multi-dimensional data in row-major
(C-style) or column-major (Fortran-style) order in
memory.
Examples
--------
>>> empty((2, 3)).tolist()
[[None, None, None], [None, None, None]]
>>> empty(2, geom_type=GeometryType.POINT).tolist()
[<POINT EMPTY>, <POINT EMPTY>]
"""
if geom_type is None:
return np.empty(shape, dtype=object, order=order)
geom_type = GeometryType(geom_type) # cast int to GeometryType
if geom_type is GeometryType.MISSING:
return np.empty(shape, dtype=object, order=order)
fill_value = from_wkt(geom_type.name + " EMPTY")
return np.full(shape, fill_value, dtype=object, order=order)
@@ -0,0 +1,82 @@
import os
from functools import wraps
import numpy as np
from shapely import lib
from shapely.errors import UnsupportedGEOSVersionError
class requires_geos:
def __init__(self, version):
if version.count(".") != 2:
raise ValueError("Version must be <major>.<minor>.<patch> format")
self.version = tuple(int(x) for x in version.split("."))
def __call__(self, func):
is_compatible = lib.geos_version >= self.version
is_doc_build = os.environ.get("SPHINX_DOC_BUILD") == "1" # set in docs/conf.py
if is_compatible and not is_doc_build:
return func # return directly, do not change the docstring
msg = "'{}' requires at least GEOS {}.{}.{}.".format(
func.__name__, *self.version
)
if is_compatible:
@wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
else:
@wraps(func)
def wrapped(*args, **kwargs):
raise UnsupportedGEOSVersionError(msg)
doc = wrapped.__doc__
if doc:
# Insert the message at the first double newline
position = doc.find("\n\n") + 2
# Figure out the indentation level
indent = 0
while True:
if doc[position + indent] == " ":
indent += 1
else:
break
wrapped.__doc__ = doc.replace(
"\n\n", "\n\n{}.. note:: {}\n\n".format(" " * indent, msg), 1
)
return wrapped
def multithreading_enabled(func):
"""Prepare multithreading by setting the writable flags of object type
ndarrays to False.
NB: multithreading also requires the GIL to be released, which is done in
the C extension (ufuncs.c)."""
@wraps(func)
def wrapped(*args, **kwargs):
array_args = [
arg for arg in args if isinstance(arg, np.ndarray) and arg.dtype == object
] + [
arg
for name, arg in kwargs.items()
if name not in {"where", "out"}
and isinstance(arg, np.ndarray)
and arg.dtype == object
]
old_flags = [arr.flags.writeable for arr in array_args]
try:
for arr in array_args:
arr.flags.writeable = False
return func(*args, **kwargs)
finally:
for arr, old_flag in zip(array_args, old_flags):
arr.flags.writeable = old_flag
return wrapped
@@ -0,0 +1,81 @@
"""Shapely errors."""
import threading
from shapely.lib import _setup_signal_checks, GEOSException, ShapelyError # NOQA
def setup_signal_checks(interval=10000):
"""This enables Python signal checks in the ufunc inner loops.
Doing so allows termination (using CTRL+C) of operations on large arrays of vectors.
Parameters
----------
interval : int, default 10000
Check for interrupts every x iterations. The higher the number, the slower
shapely will respond to a signal. However, at low values there will be a negative effect
on performance. The default of 10000 does not have any measureable effects on performance.
Notes
-----
For more information on signals consult the Python docs:
https://docs.python.org/3/library/signal.html
"""
if interval <= 0:
raise ValueError("Signal checks interval must be greater than zero.")
_setup_signal_checks(interval, threading.main_thread().ident)
class UnsupportedGEOSVersionError(ShapelyError):
"""Raised when the GEOS library version does not support a certain operation."""
class DimensionError(ShapelyError):
"""An error in the number of coordinate dimensions."""
class TopologicalError(ShapelyError):
"""A geometry is invalid or topologically incorrect."""
class ShapelyDeprecationWarning(FutureWarning):
"""
Warning for features that will be removed or behaviour that will be
changed in a future release.
"""
class EmptyPartError(ShapelyError):
"""An error signifying an empty part was encountered when creating a multi-part."""
class GeometryTypeError(ShapelyError):
"""
An error raised when the type of the geometry in question is
unrecognized or inappropriate.
"""
def __getattr__(name):
import warnings
# Alias Shapely 1.8 error classes to ShapelyError with deprecation warning
if name in [
"ReadingError",
"WKBReadingError",
"WKTReadingError",
"PredicateError",
"InvalidGeometryError",
]:
warnings.warn(
f"{name} is deprecated and will be removed in a future version. "
"Use ShapelyError instead (functions previously raising {name} "
"will now raise a ShapelyError instead).",
DeprecationWarning,
stacklevel=2,
)
return ShapelyError
raise AttributeError(f"module 'shapely.errors' has no attribute '{name}'")
@@ -0,0 +1,28 @@
"""Geometry classes and factories
"""
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
from shapely.geometry.collection import GeometryCollection
from shapely.geometry.geo import box, mapping, shape
from shapely.geometry.linestring import LineString
from shapely.geometry.multilinestring import MultiLineString
from shapely.geometry.multipoint import MultiPoint
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry.point import Point
from shapely.geometry.polygon import LinearRing, Polygon
__all__ = [
"box",
"shape",
"mapping",
"Point",
"LineString",
"Polygon",
"MultiPoint",
"MultiLineString",
"MultiPolygon",
"GeometryCollection",
"LinearRing",
"CAP_STYLE",
"JOIN_STYLE",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
"""Multi-part collections of geometries
"""
import shapely
from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
class GeometryCollection(BaseMultipartGeometry):
"""
A collection of one or more geometries that may contain more than one type
of geometry.
Parameters
----------
geoms : list
A list of shapely geometry instances, which may be of varying
geometry types.
Attributes
----------
geoms : sequence
A sequence of Shapely geometry instances
Examples
--------
Create a GeometryCollection with a Point and a LineString
>>> from shapely import LineString, Point
>>> p = Point(51, -1)
>>> l = LineString([(52, -1), (49, 2)])
>>> gc = GeometryCollection([p, l])
"""
__slots__ = []
def __new__(self, geoms=None):
if not geoms:
# TODO better empty constructor
return shapely.from_wkt("GEOMETRYCOLLECTION EMPTY")
if isinstance(geoms, BaseGeometry):
# TODO(shapely-2.0) do we actually want to split Multi-part geometries?
# this is needed for the split() tests
if hasattr(geoms, "geoms"):
geoms = geoms.geoms
else:
geoms = [geoms]
return shapely.geometrycollections(geoms)
@property
def __geo_interface__(self):
geometries = []
for geom in self.geoms:
geometries.append(geom.__geo_interface__)
return dict(type="GeometryCollection", geometries=geometries)
shapely.lib.registry[7] = GeometryCollection
@@ -0,0 +1,10 @@
"""Autouse fixtures for doctests."""
import pytest
from shapely.geometry.linestring import LineString
@pytest.fixture(autouse=True)
def add_linestring(doctest_namespace):
doctest_namespace["LineString"] = LineString
@@ -0,0 +1,135 @@
"""
Geometry factories based on the geo interface
"""
import numpy as np
from shapely.errors import GeometryTypeError
from shapely.geometry.collection import GeometryCollection
from shapely.geometry.linestring import LineString
from shapely.geometry.multilinestring import MultiLineString
from shapely.geometry.multipoint import MultiPoint
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry.point import Point
from shapely.geometry.polygon import LinearRing, Polygon
def _is_coordinates_empty(coordinates):
"""Helper to identify if coordinates or subset of coordinates are empty"""
if coordinates is None:
return True
if isinstance(coordinates, (list, tuple, np.ndarray)):
if len(coordinates) == 0:
return True
return all(map(_is_coordinates_empty, coordinates))
else:
return False
def _empty_shape_for_no_coordinates(geom_type):
"""Return empty counterpart for geom_type"""
if geom_type == "point":
return Point()
elif geom_type == "multipoint":
return MultiPoint()
elif geom_type == "linestring":
return LineString()
elif geom_type == "multilinestring":
return MultiLineString()
elif geom_type == "polygon":
return Polygon()
elif geom_type == "multipolygon":
return MultiPolygon()
else:
raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}")
def box(minx, miny, maxx, maxy, ccw=True):
"""Returns a rectangular polygon with configurable normal vector"""
coords = [(maxx, miny), (maxx, maxy), (minx, maxy), (minx, miny)]
if not ccw:
coords = coords[::-1]
return Polygon(coords)
def shape(context):
"""
Returns a new, independent geometry with coordinates *copied* from the
context. Changes to the original context will not be reflected in the
geometry object.
Parameters
----------
context :
a GeoJSON-like dict, which provides a "type" member describing the type
of the geometry and "coordinates" member providing a list of coordinates,
or an object which implements __geo_interface__.
Returns
-------
Geometry object
Examples
--------
Create a Point from GeoJSON, and then create a copy using __geo_interface__.
>>> context = {'type': 'Point', 'coordinates': [0, 1]}
>>> geom = shape(context)
>>> geom.geom_type == 'Point'
True
>>> geom.wkt
'POINT (0 1)'
>>> geom2 = shape(geom)
>>> geom == geom2
True
"""
if hasattr(context, "__geo_interface__"):
ob = context.__geo_interface__
else:
ob = context
geom_type = ob.get("type").lower()
if "coordinates" in ob and _is_coordinates_empty(ob["coordinates"]):
return _empty_shape_for_no_coordinates(geom_type)
elif geom_type == "point":
return Point(ob["coordinates"])
elif geom_type == "linestring":
return LineString(ob["coordinates"])
elif geom_type == "linearring":
return LinearRing(ob["coordinates"])
elif geom_type == "polygon":
return Polygon(ob["coordinates"][0], ob["coordinates"][1:])
elif geom_type == "multipoint":
return MultiPoint(ob["coordinates"])
elif geom_type == "multilinestring":
return MultiLineString(ob["coordinates"])
elif geom_type == "multipolygon":
return MultiPolygon([[c[0], c[1:]] for c in ob["coordinates"]])
elif geom_type == "geometrycollection":
geoms = [shape(g) for g in ob.get("geometries", [])]
return GeometryCollection(geoms)
else:
raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}")
def mapping(ob):
"""
Returns a GeoJSON-like mapping from a Geometry or any
object which implements __geo_interface__
Parameters
----------
ob :
An object which implements __geo_interface__.
Returns
-------
dict
Examples
--------
>>> pt = Point(0, 0)
>>> mapping(pt)
{'type': 'Point', 'coordinates': (0.0, 0.0)}
"""
return ob.__geo_interface__
@@ -0,0 +1,188 @@
"""Line strings and related utilities
"""
import numpy as np
import shapely
from shapely.geometry.base import BaseGeometry, JOIN_STYLE
from shapely.geometry.point import Point
__all__ = ["LineString"]
class LineString(BaseGeometry):
"""
A geometry type composed of one or more line segments.
A LineString is a one-dimensional feature and has a non-zero length but
zero area. It may approximate a curve and need not be straight. Unlike a
LinearRing, a LineString is not closed.
Parameters
----------
coordinates : sequence
A sequence of (x, y, [,z]) numeric coordinate pairs or triples, or
an array-like with shape (N, 2) or (N, 3).
Also can be a sequence of Point objects.
Examples
--------
Create a LineString with two segments
>>> a = LineString([[0, 0], [1, 0], [1, 1]])
>>> a.length
2.0
"""
__slots__ = []
def __new__(self, coordinates=None):
if coordinates is None:
# empty geometry
# TODO better constructor
return shapely.from_wkt("LINESTRING EMPTY")
elif isinstance(coordinates, LineString):
if type(coordinates) == LineString:
# return original objects since geometries are immutable
return coordinates
else:
# LinearRing
# TODO convert LinearRing to LineString more directly
coordinates = coordinates.coords
else:
if hasattr(coordinates, "__array__"):
coordinates = np.asarray(coordinates)
if isinstance(coordinates, np.ndarray) and np.issubdtype(
coordinates.dtype, np.number
):
pass
else:
# check coordinates on points
def _coords(o):
if isinstance(o, Point):
return o.coords[0]
else:
return [float(c) for c in o]
coordinates = [_coords(o) for o in coordinates]
if len(coordinates) == 0:
# empty geometry
# TODO better constructor + should shapely.linestrings handle this?
return shapely.from_wkt("LINESTRING EMPTY")
geom = shapely.linestrings(coordinates)
if not isinstance(geom, LineString):
raise ValueError("Invalid values passed to LineString constructor")
return geom
@property
def __geo_interface__(self):
return {"type": "LineString", "coordinates": tuple(self.coords)}
def svg(self, scale_factor=1.0, stroke_color=None, opacity=None):
"""Returns SVG polyline element for the LineString geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG stroke-width. Default is 1.
stroke_color : str, optional
Hex string for stroke color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.8
"""
if self.is_empty:
return "<g />"
if stroke_color is None:
stroke_color = "#66cc99" if self.is_valid else "#ff3333"
if opacity is None:
opacity = 0.8
pnt_format = " ".join(["{},{}".format(*c) for c in self.coords])
return (
'<polyline fill="none" stroke="{2}" stroke-width="{1}" '
'points="{0}" opacity="{3}" />'
).format(pnt_format, 2.0 * scale_factor, stroke_color, opacity)
@property
def xy(self):
"""Separate arrays of X and Y coordinate values
Example:
>>> x, y = LineString([(0, 0), (1, 1)]).xy
>>> list(x)
[0.0, 1.0]
>>> list(y)
[0.0, 1.0]
"""
return self.coords.xy
def offset_curve(
self,
distance,
quad_segs=16,
join_style=JOIN_STYLE.round,
mitre_limit=5.0,
):
"""Returns a LineString or MultiLineString geometry at a distance from
the object on its right or its left side.
The side is determined by the sign of the `distance` parameter
(negative for right side offset, positive for left side offset). The
resolution of the buffer around each vertex of the object increases
by increasing the `quad_segs` keyword parameter.
The join style is for outside corners between line segments. Accepted
values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and
JOIN_STYLE.bevel (3).
The mitre ratio limit is used for very sharp corners. It is the ratio
of the distance from the corner to the end of the mitred offset corner.
When two line segments meet at a sharp angle, a miter join will extend
far beyond the original geometry. To prevent unreasonable geometry, the
mitre limit allows controlling the maximum length of the join corner.
Corners with a ratio which exceed the limit will be beveled.
Note: the behaviour regarding orientation of the resulting line
depends on the GEOS version. With GEOS < 3.11, the line retains the
same direction for a left offset (positive distance) or has reverse
direction for a right offset (negative distance), and this behaviour
was documented as such in previous Shapely versions. Starting with
GEOS 3.11, the function tries to preserve the orientation of the
original line.
"""
if mitre_limit == 0.0:
raise ValueError("Cannot compute offset from zero-length line segment")
elif not np.isfinite(distance):
raise ValueError("offset_curve distance must be finite")
return shapely.offset_curve(self, distance, quad_segs, join_style, mitre_limit)
def parallel_offset(
self,
distance,
side="right",
resolution=16,
join_style=JOIN_STYLE.round,
mitre_limit=5.0,
):
"""
Alternative method to :meth:`offset_curve` method.
Older alternative method to the :meth:`offset_curve` method, but uses
``resolution`` instead of ``quad_segs`` and a ``side`` keyword
('left' or 'right') instead of sign of the distance. This method is
kept for backwards compatibility for now, but is is recommended to
use :meth:`offset_curve` instead.
"""
if side == "right":
distance *= -1
return self.offset_curve(
distance,
quad_segs=resolution,
join_style=join_style,
mitre_limit=mitre_limit,
)
shapely.lib.registry[1] = LineString
@@ -0,0 +1,93 @@
"""Collections of linestrings and related utilities
"""
import shapely
from shapely.errors import EmptyPartError
from shapely.geometry import linestring
from shapely.geometry.base import BaseMultipartGeometry
__all__ = ["MultiLineString"]
class MultiLineString(BaseMultipartGeometry):
"""
A collection of one or more LineStrings.
A MultiLineString has non-zero length and zero area.
Parameters
----------
lines : sequence
A sequence LineStrings, or a sequence of line-like coordinate
sequences or array-likes (see accepted input for LineString).
Attributes
----------
geoms : sequence
A sequence of LineStrings
Examples
--------
Construct a MultiLineString containing two LineStrings.
>>> lines = MultiLineString([[[0, 0], [1, 2]], [[4, 4], [5, 6]]])
"""
__slots__ = []
def __new__(self, lines=None):
if not lines:
# allow creation of empty multilinestrings, to support unpickling
# TODO better empty constructor
return shapely.from_wkt("MULTILINESTRING EMPTY")
elif isinstance(lines, MultiLineString):
return lines
lines = getattr(lines, "geoms", lines)
m = len(lines)
subs = []
for i in range(m):
line = linestring.LineString(lines[i])
if line.is_empty:
raise EmptyPartError(
"Can't create MultiLineString with empty component"
)
subs.append(line)
if len(lines) == 0:
return shapely.from_wkt("MULTILINESTRING EMPTY")
return shapely.multilinestrings(subs)
@property
def __geo_interface__(self):
return {
"type": "MultiLineString",
"coordinates": tuple(tuple(c for c in g.coords) for g in self.geoms),
}
def svg(self, scale_factor=1.0, stroke_color=None, opacity=None):
"""Returns a group of SVG polyline elements for the LineString geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG stroke-width. Default is 1.
stroke_color : str, optional
Hex string for stroke color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.8
"""
if self.is_empty:
return "<g />"
if stroke_color is None:
stroke_color = "#66cc99" if self.is_valid else "#ff3333"
return (
"<g>"
+ "".join(p.svg(scale_factor, stroke_color, opacity) for p in self.geoms)
+ "</g>"
)
shapely.lib.registry[5] = MultiLineString
@@ -0,0 +1,95 @@
"""Collections of points and related utilities
"""
import shapely
from shapely.errors import EmptyPartError
from shapely.geometry import point
from shapely.geometry.base import BaseMultipartGeometry
__all__ = ["MultiPoint"]
class MultiPoint(BaseMultipartGeometry):
"""
A collection of one or more Points.
A MultiPoint has zero area and zero length.
Parameters
----------
points : sequence
A sequence of Points, or a sequence of (x, y [,z]) numeric coordinate
pairs or triples, or an array-like of shape (N, 2) or (N, 3).
Attributes
----------
geoms : sequence
A sequence of Points
Examples
--------
Construct a MultiPoint containing two Points
>>> from shapely import Point
>>> ob = MultiPoint([[0.0, 0.0], [1.0, 2.0]])
>>> len(ob.geoms)
2
>>> type(ob.geoms[0]) == Point
True
"""
__slots__ = []
def __new__(self, points=None):
if points is None:
# allow creation of empty multipoints, to support unpickling
# TODO better empty constructor
return shapely.from_wkt("MULTIPOINT EMPTY")
elif isinstance(points, MultiPoint):
return points
m = len(points)
subs = []
for i in range(m):
p = point.Point(points[i])
if p.is_empty:
raise EmptyPartError("Can't create MultiPoint with empty component")
subs.append(p)
if len(points) == 0:
return shapely.from_wkt("MULTIPOINT EMPTY")
return shapely.multipoints(subs)
@property
def __geo_interface__(self):
return {
"type": "MultiPoint",
"coordinates": tuple(g.coords[0] for g in self.geoms),
}
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
"""Returns a group of SVG circle elements for the MultiPoint geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG circle diameters. Default is 1.
fill_color : str, optional
Hex string for fill color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.6
"""
if self.is_empty:
return "<g />"
if fill_color is None:
fill_color = "#66cc99" if self.is_valid else "#ff3333"
return (
"<g>"
+ "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms)
+ "</g>"
)
shapely.lib.registry[4] = MultiPoint
@@ -0,0 +1,126 @@
"""Collections of polygons and related utilities
"""
import shapely
from shapely.geometry import polygon
from shapely.geometry.base import BaseMultipartGeometry
__all__ = ["MultiPolygon"]
class MultiPolygon(BaseMultipartGeometry):
"""
A collection of one or more Polygons.
If component polygons overlap the collection is invalid and some
operations on it may fail.
Parameters
----------
polygons : sequence
A sequence of Polygons, or a sequence of (shell, holes) tuples
where shell is the sequence representation of a linear ring
(see LinearRing) and holes is a sequence of such linear rings.
Attributes
----------
geoms : sequence
A sequence of `Polygon` instances
Examples
--------
Construct a MultiPolygon from a sequence of coordinate tuples
>>> from shapely import Polygon
>>> ob = MultiPolygon([
... (
... ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
... [((0.1,0.1), (0.1,0.2), (0.2,0.2), (0.2,0.1))]
... )
... ])
>>> len(ob.geoms)
1
>>> type(ob.geoms[0]) == Polygon
True
"""
__slots__ = []
def __new__(self, polygons=None):
if not polygons:
# allow creation of empty multipolygons, to support unpickling
# TODO better empty constructor
return shapely.from_wkt("MULTIPOLYGON EMPTY")
elif isinstance(polygons, MultiPolygon):
return polygons
polygons = getattr(polygons, "geoms", polygons)
polygons = [
p
for p in polygons
if p and not (isinstance(p, polygon.Polygon) and p.is_empty)
]
L = len(polygons)
# Bail immediately if we have no input points.
if L == 0:
return shapely.from_wkt("MULTIPOLYGON EMPTY")
# This function does not accept sequences of MultiPolygons: there is
# no implicit flattening.
if isinstance(polygons[0], MultiPolygon):
raise ValueError("Sequences of multi-polygons are not valid arguments")
subs = []
for i in range(L):
ob = polygons[i]
if not isinstance(ob, polygon.Polygon):
shell = ob[0]
if len(ob) > 1:
holes = ob[1]
else:
holes = None
p = polygon.Polygon(shell, holes)
else:
p = polygon.Polygon(ob)
subs.append(p)
return shapely.multipolygons(subs)
@property
def __geo_interface__(self):
allcoords = []
for geom in self.geoms:
coords = []
coords.append(tuple(geom.exterior.coords))
for hole in geom.interiors:
coords.append(tuple(hole.coords))
allcoords.append(tuple(coords))
return {"type": "MultiPolygon", "coordinates": allcoords}
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
"""Returns group of SVG path elements for the MultiPolygon geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG stroke-width. Default is 1.
fill_color : str, optional
Hex string for fill color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.6
"""
if self.is_empty:
return "<g />"
if fill_color is None:
fill_color = "#66cc99" if self.is_valid else "#ff3333"
return (
"<g>"
+ "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms)
+ "</g>"
)
shapely.lib.registry[6] = MultiPolygon
@@ -0,0 +1,145 @@
"""Points and related utilities
"""
import numpy as np
import shapely
from shapely.errors import DimensionError
from shapely.geometry.base import BaseGeometry
__all__ = ["Point"]
class Point(BaseGeometry):
"""
A geometry type that represents a single coordinate with
x,y and possibly z values.
A point is a zero-dimensional feature and has zero length and zero area.
Parameters
----------
args : float, or sequence of floats
The coordinates can either be passed as a single parameter, or as
individual float values using multiple parameters:
1) 1 parameter: a sequence or array-like of with 2 or 3 values.
2) 2 or 3 parameters (float): x, y, and possibly z.
Attributes
----------
x, y, z : float
Coordinate values
Examples
--------
Constructing the Point using separate parameters for x and y:
>>> p = Point(1.0, -1.0)
Constructing the Point using a list of x, y coordinates:
>>> p = Point([1.0, -1.0])
>>> print(p)
POINT (1 -1)
>>> p.y
-1.0
>>> p.x
1.0
"""
__slots__ = []
def __new__(self, *args):
if len(args) == 0:
# empty geometry
# TODO better constructor
return shapely.from_wkt("POINT EMPTY")
elif len(args) > 3:
raise TypeError(f"Point() takes at most 3 arguments ({len(args)} given)")
elif len(args) == 1:
coords = args[0]
if isinstance(coords, Point):
return coords
# Accept either (x, y) or [(x, y)]
if not hasattr(coords, "__getitem__"): # generators
coords = list(coords)
coords = np.asarray(coords).squeeze()
else:
# 2 or 3 args
coords = np.array(args).squeeze()
if coords.ndim > 1:
raise ValueError(
f"Point() takes only scalar or 1-size vector arguments, got {args}"
)
if not np.issubdtype(coords.dtype, np.number):
coords = [float(c) for c in coords]
geom = shapely.points(coords)
if not isinstance(geom, Point):
raise ValueError("Invalid values passed to Point constructor")
return geom
# Coordinate getters and setters
@property
def x(self):
"""Return x coordinate."""
return float(shapely.get_x(self))
@property
def y(self):
"""Return y coordinate."""
return float(shapely.get_y(self))
@property
def z(self):
"""Return z coordinate."""
if not shapely.has_z(self):
raise DimensionError("This point has no z coordinate.")
# return shapely.get_z(self) -> get_z only supported for GEOS 3.7+
return self.coords[0][2]
@property
def __geo_interface__(self):
return {"type": "Point", "coordinates": self.coords[0]}
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
"""Returns SVG circle element for the Point geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG circle diameter. Default is 1.
fill_color : str, optional
Hex string for fill color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.6
"""
if self.is_empty:
return "<g />"
if fill_color is None:
fill_color = "#66cc99" if self.is_valid else "#ff3333"
if opacity is None:
opacity = 0.6
return (
'<circle cx="{0.x}" cy="{0.y}" r="{1}" '
'stroke="#555555" stroke-width="{2}" fill="{3}" opacity="{4}" />'
).format(self, 3.0 * scale_factor, 1.0 * scale_factor, fill_color, opacity)
@property
def xy(self):
"""Separate arrays of X and Y coordinate values
Example:
>>> x, y = Point(0, 0).xy
>>> list(x)
[0.0]
>>> list(y)
[0.0]
"""
return self.coords.xy
shapely.lib.registry[0] = Point
@@ -0,0 +1,355 @@
"""Polygons and their linear ring components
"""
import numpy as np
import shapely
from shapely.algorithms.cga import is_ccw_impl, signed_area
from shapely.errors import TopologicalError
from shapely.geometry.base import BaseGeometry
from shapely.geometry.linestring import LineString
from shapely.geometry.point import Point
__all__ = ["orient", "Polygon", "LinearRing"]
def _unpickle_linearring(wkb):
linestring = shapely.from_wkb(wkb)
srid = shapely.get_srid(linestring)
linearring = shapely.linearrings(shapely.get_coordinates(linestring))
if srid:
linearring = shapely.set_srid(linearring, srid)
return linearring
class LinearRing(LineString):
"""
A geometry type composed of one or more line segments
that forms a closed loop.
A LinearRing is a closed, one-dimensional feature.
A LinearRing that crosses itself or touches itself at a single point is
invalid and operations on it may fail.
Parameters
----------
coordinates : sequence
A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
an array-like with shape (N, 2) or (N, 3).
Also can be a sequence of Point objects.
Notes
-----
Rings are automatically closed. There is no need to specify a final
coordinate pair identical to the first.
Examples
--------
Construct a square ring.
>>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) )
>>> ring.is_closed
True
>>> list(ring.coords)
[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
>>> ring.length
4.0
"""
__slots__ = []
def __new__(self, coordinates=None):
if coordinates is None:
# empty geometry
# TODO better way?
return shapely.from_wkt("LINEARRING EMPTY")
elif isinstance(coordinates, LineString):
if type(coordinates) == LinearRing:
# return original objects since geometries are immutable
return coordinates
elif not coordinates.is_valid:
raise TopologicalError("An input LineString must be valid.")
else:
# LineString
# TODO convert LineString to LinearRing more directly?
coordinates = coordinates.coords
else:
if hasattr(coordinates, "__array__"):
coordinates = np.asarray(coordinates)
if isinstance(coordinates, np.ndarray) and np.issubdtype(
coordinates.dtype, np.number
):
pass
else:
# check coordinates on points
def _coords(o):
if isinstance(o, Point):
return o.coords[0]
else:
return [float(c) for c in o]
coordinates = np.array([_coords(o) for o in coordinates])
if not np.issubdtype(coordinates.dtype, np.number):
# conversion of coords to 2D array failed, this might be due
# to inconsistent coordinate dimensionality
raise ValueError("Inconsistent coordinate dimensionality")
if len(coordinates) == 0:
# empty geometry
# TODO better constructor + should shapely.linearrings handle this?
return shapely.from_wkt("LINEARRING EMPTY")
geom = shapely.linearrings(coordinates)
if not isinstance(geom, LinearRing):
raise ValueError("Invalid values passed to LinearRing constructor")
return geom
@property
def __geo_interface__(self):
return {"type": "LinearRing", "coordinates": tuple(self.coords)}
def __reduce__(self):
"""WKB doesn't differentiate between LineString and LinearRing so we
need to move the coordinate sequence into the correct geometry type"""
return (_unpickle_linearring, (shapely.to_wkb(self, include_srid=True),))
@property
def is_ccw(self):
"""True is the ring is oriented counter clock-wise"""
return bool(is_ccw_impl()(self))
@property
def is_simple(self):
"""True if the geometry is simple, meaning that any self-intersections
are only at boundary points, else False"""
return bool(shapely.is_simple(self))
shapely.lib.registry[2] = LinearRing
class InteriorRingSequence:
_parent = None
_ndim = None
_index = 0
_length = 0
def __init__(self, parent):
self._parent = parent
self._ndim = parent._ndim
def __iter__(self):
self._index = 0
self._length = self.__len__()
return self
def __next__(self):
if self._index < self._length:
ring = self._get_ring(self._index)
self._index += 1
return ring
else:
raise StopIteration
def __len__(self):
return shapely.get_num_interior_rings(self._parent)
def __getitem__(self, key):
m = self.__len__()
if isinstance(key, int):
if key + m < 0 or key >= m:
raise IndexError("index out of range")
if key < 0:
i = m + key
else:
i = key
return self._get_ring(i)
elif isinstance(key, slice):
res = []
start, stop, stride = key.indices(m)
for i in range(start, stop, stride):
res.append(self._get_ring(i))
return res
else:
raise TypeError("key must be an index or slice")
def _get_ring(self, i):
return shapely.get_interior_ring(self._parent, i)
class Polygon(BaseGeometry):
"""
A geometry type representing an area that is enclosed by a linear ring.
A polygon is a two-dimensional feature and has a non-zero area. It may
have one or more negative-space "holes" which are also bounded by linear
rings. If any rings cross each other, the feature is invalid and
operations on it may fail.
Parameters
----------
shell : sequence
A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
an array-like with shape (N, 2) or (N, 3).
Also can be a sequence of Point objects.
holes : sequence
A sequence of objects which satisfy the same requirements as the
shell parameters above
Attributes
----------
exterior : LinearRing
The ring which bounds the positive space of the polygon.
interiors : sequence
A sequence of rings which bound all existing holes.
Examples
--------
Create a square polygon with no holes
>>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.))
>>> polygon = Polygon(coords)
>>> polygon.area
1.0
"""
__slots__ = []
def __new__(self, shell=None, holes=None):
if shell is None:
# empty geometry
# TODO better way?
return shapely.from_wkt("POLYGON EMPTY")
elif isinstance(shell, Polygon):
# return original objects since geometries are immutable
return shell
else:
shell = LinearRing(shell)
if holes is not None:
if len(holes) == 0:
# shapely constructor cannot handle holes=[]
holes = None
else:
holes = [LinearRing(ring) for ring in holes]
geom = shapely.polygons(shell, holes=holes)
if not isinstance(geom, Polygon):
raise ValueError("Invalid values passed to Polygon constructor")
return geom
@property
def exterior(self):
return shapely.get_exterior_ring(self)
@property
def interiors(self):
if self.is_empty:
return []
return InteriorRingSequence(self)
@property
def coords(self):
raise NotImplementedError(
"Component rings have coordinate sequences, but the polygon does not"
)
def __eq__(self, other):
if not isinstance(other, BaseGeometry):
return NotImplemented
if not isinstance(other, Polygon):
return False
check_empty = (self.is_empty, other.is_empty)
if all(check_empty):
return True
elif any(check_empty):
return False
my_coords = [self.exterior.coords] + [
interior.coords for interior in self.interiors
]
other_coords = [other.exterior.coords] + [
interior.coords for interior in other.interiors
]
if not len(my_coords) == len(other_coords):
return False
# equal_nan=False is the default, but not yet available for older numpy
return np.all(
[
np.array_equal(left, right) # , equal_nan=False)
for left, right in zip(my_coords, other_coords)
]
)
def __hash__(self):
return super().__hash__()
@property
def __geo_interface__(self):
if self.exterior == LinearRing():
coords = []
else:
coords = [tuple(self.exterior.coords)]
for hole in self.interiors:
coords.append(tuple(hole.coords))
return {"type": "Polygon", "coordinates": tuple(coords)}
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
"""Returns SVG path element for the Polygon geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG stroke-width. Default is 1.
fill_color : str, optional
Hex string for fill color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
opacity : float
Float number between 0 and 1 for color opacity. Default value is 0.6
"""
if self.is_empty:
return "<g />"
if fill_color is None:
fill_color = "#66cc99" if self.is_valid else "#ff3333"
if opacity is None:
opacity = 0.6
exterior_coords = [["{},{}".format(*c) for c in self.exterior.coords]]
interior_coords = [
["{},{}".format(*c) for c in interior.coords] for interior in self.interiors
]
path = " ".join(
[
"M {} L {} z".format(coords[0], " L ".join(coords[1:]))
for coords in exterior_coords + interior_coords
]
)
return (
'<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
'stroke-width="{0}" opacity="{3}" d="{1}" />'
).format(2.0 * scale_factor, path, fill_color, opacity)
@classmethod
def from_bounds(cls, xmin, ymin, xmax, ymax):
"""Construct a `Polygon()` from spatial bounds."""
return cls([(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
shapely.lib.registry[3] = Polygon
def orient(polygon, sign=1.0):
s = float(sign)
rings = []
ring = polygon.exterior
if signed_area(ring) / s >= 0.0:
rings.append(ring)
else:
rings.append(list(ring.coords)[::-1])
for ring in polygon.interiors:
if signed_area(ring) / s <= 0.0:
rings.append(ring)
else:
rings.append(list(ring.coords)[::-1])
return Polygon(rings[0], rings[1:])
@@ -0,0 +1,8 @@
"""
Proxies for libgeos, GEOS-specific exceptions, and utilities
"""
import shapely
geos_version_string = shapely.geos_capi_version_string
geos_version = shapely.geos_version
geos_capi_version = shapely.geos_capi_version
@@ -0,0 +1,372 @@
import numpy as np
from shapely import lib
from shapely._enum import ParamEnum
# include ragged array functions here for reference documentation purpose
from shapely._ragged_array import from_ragged_array, to_ragged_array
from shapely.decorators import requires_geos
from shapely.errors import UnsupportedGEOSVersionError
__all__ = [
"from_geojson",
"from_ragged_array",
"from_wkb",
"from_wkt",
"to_geojson",
"to_ragged_array",
"to_wkb",
"to_wkt",
]
# Allowed options for handling WKB/WKT decoding errors
# Note: cannot use standard constructor since "raise" is a keyword
DecodingErrorOptions = ParamEnum(
"DecodingErrorOptions", {"ignore": 0, "warn": 1, "raise": 2}
)
WKBFlavorOptions = ParamEnum("WKBFlavorOptions", {"extended": 1, "iso": 2})
def to_wkt(
geometry,
rounding_precision=6,
trim=True,
output_dimension=3,
old_3d=False,
**kwargs,
):
"""
Converts to the Well-Known Text (WKT) representation of a Geometry.
The Well-known Text format is defined in the `OGC Simple Features
Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
The following limitations apply to WKT serialization:
- for GEOS <= 3.8 a multipoint with an empty sub-geometry will raise an exception
- for GEOS <= 3.8 empty geometries are always serialized to 2D
- for GEOS >= 3.9 only simple empty geometries can be 3D, collections are still
always 2D
Parameters
----------
geometry : Geometry or array_like
rounding_precision : int, default 6
The rounding precision when writing the WKT string. Set to a value of
-1 to indicate the full precision.
trim : bool, default True
If True, trim unnecessary decimals (trailing zeros).
output_dimension : int, default 3
The output dimension for the WKT string. 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 WKT string.
old_3d : bool, default False
Enable old style 3D/4D WKT generation. By default, new style 3D/4D WKT
(ie. "POINT Z (10 20 30)") is returned, but with ``old_3d=True``
the WKT will be formatted in the style "POINT (10 20 30)".
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import Point
>>> to_wkt(Point(0, 0))
'POINT (0 0)'
>>> to_wkt(Point(0, 0), rounding_precision=3, trim=False)
'POINT (0.000 0.000)'
>>> to_wkt(Point(0, 0), rounding_precision=-1, trim=False)
'POINT (0.0000000000000000 0.0000000000000000)'
>>> to_wkt(Point(1, 2, 3), trim=True)
'POINT Z (1 2 3)'
>>> to_wkt(Point(1, 2, 3), trim=True, output_dimension=2)
'POINT (1 2)'
>>> to_wkt(Point(1, 2, 3), trim=True, old_3d=True)
'POINT (1 2 3)'
Notes
-----
The defaults differ from the default of the GEOS library. To mimic this,
use::
to_wkt(geometry, rounding_precision=-1, trim=False, output_dimension=2)
"""
if not np.isscalar(rounding_precision):
raise TypeError("rounding_precision only accepts scalar values")
if not np.isscalar(trim):
raise TypeError("trim only accepts scalar values")
if not np.isscalar(output_dimension):
raise TypeError("output_dimension only accepts scalar values")
if not np.isscalar(old_3d):
raise TypeError("old_3d only accepts scalar values")
return lib.to_wkt(
geometry,
np.intc(rounding_precision),
np.bool_(trim),
np.intc(output_dimension),
np.bool_(old_3d),
**kwargs,
)
def to_wkb(
geometry,
hex=False,
output_dimension=3,
byte_order=-1,
include_srid=False,
flavor="extended",
**kwargs,
):
r"""
Converts to the Well-Known Binary (WKB) representation of a Geometry.
The Well-Known Binary format is defined in the `OGC Simple Features
Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
The following limitations apply to WKB serialization:
- linearrings will be converted to linestrings
- a point with only NaN coordinates is converted to an empty point
- for GEOS <= 3.7, empty points are always serialized to 3D if
output_dimension=3, and to 2D if output_dimension=2
- for GEOS == 3.8, empty points are always serialized to 2D
Parameters
----------
geometry : Geometry or array_like
hex : bool, default False
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 representation.
byte_order : int, default -1
Defaults to native machine byte order (-1). Use 0 to force big endian
and 1 for little endian.
include_srid : bool, default False
If True, the SRID is be included in WKB (this is an extension
to the OGC WKB specification). Not allowed when flavor is "iso".
flavor : {"iso", "extended"}, default "extended"
Which flavor of WKB will be returned. The flavor determines how
extra dimensionality is encoded with the type number, and whether
SRID can be included in the WKB. ISO flavor is "more standard" for
3D output, and does not support SRID embedding.
Both flavors are equivalent when ``output_dimension=2`` (or with 2D
geometries) and ``include_srid=False``.
The `from_wkb` function can read both flavors.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import Point
>>> point = Point(1, 1)
>>> to_wkb(point, byte_order=1)
b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?'
>>> to_wkb(point, hex=True, byte_order=1)
'0101000000000000000000F03F000000000000F03F'
"""
if not np.isscalar(hex):
raise TypeError("hex only accepts scalar values")
if not np.isscalar(output_dimension):
raise TypeError("output_dimension only accepts scalar values")
if not np.isscalar(byte_order):
raise TypeError("byte_order only accepts scalar values")
if not np.isscalar(include_srid):
raise TypeError("include_srid only accepts scalar values")
if not np.isscalar(flavor):
raise TypeError("flavor only accepts scalar values")
if lib.geos_version < (3, 10, 0) and flavor == "iso":
raise UnsupportedGEOSVersionError(
'The "iso" option requires at least GEOS 3.10.0'
)
if flavor == "iso" and include_srid:
raise ValueError('flavor="iso" and include_srid=True cannot be used together')
flavor = WKBFlavorOptions.get_value(flavor)
return lib.to_wkb(
geometry,
np.bool_(hex),
np.intc(output_dimension),
np.intc(byte_order),
np.bool_(include_srid),
np.intc(flavor),
**kwargs,
)
@requires_geos("3.10.0")
def to_geojson(geometry, indent=None, **kwargs):
"""Converts to the GeoJSON representation of a Geometry.
The GeoJSON format is defined in the `RFC 7946 <https://geojson.org/>`__.
NaN (not-a-number) coordinates will be written as 'null'.
The following are currently unsupported:
- Geometries of type LINEARRING: these are output as 'null'.
- Three-dimensional geometries: the third dimension is ignored.
Parameters
----------
geometry : str, bytes or array_like
indent : int, optional
If indent is a non-negative integer, then GeoJSON will be formatted.
An indent level of 0 will only insert newlines. None (the default)
selects the most compact representation.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import Point
>>> point = Point(1, 1)
>>> to_geojson(point)
'{"type":"Point","coordinates":[1.0,1.0]}'
>>> print(to_geojson(point, indent=2))
{
"type": "Point",
"coordinates": [
1.0,
1.0
]
}
"""
# GEOS Tickets:
# - handle linearrings: https://trac.osgeo.org/geos/ticket/1140
# - support 3D: https://trac.osgeo.org/geos/ticket/1141
if indent is None:
indent = -1
elif not np.isscalar(indent):
raise TypeError("indent only accepts scalar values")
elif indent < 0:
raise ValueError("indent cannot be negative")
return lib.to_geojson(geometry, np.intc(indent), **kwargs)
def from_wkt(geometry, on_invalid="raise", **kwargs):
"""
Creates geometries from the Well-Known Text (WKT) representation.
The Well-known Text format is defined in the `OGC Simple Features
Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
Parameters
----------
geometry : str or array_like
The WKT string(s) to convert.
on_invalid : {"raise", "warn", "ignore"}, default "raise"
- raise: an exception will be raised if WKT input geometries are invalid.
- warn: a warning will be raised and invalid WKT geometries will be
returned as ``None``.
- ignore: invalid WKT geometries will be returned as ``None`` without a warning.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from_wkt('POINT (0 0)')
<POINT (0 0)>
"""
if not np.isscalar(on_invalid):
raise TypeError("on_invalid only accepts scalar values")
invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid))
return lib.from_wkt(geometry, invalid_handler, **kwargs)
def from_wkb(geometry, on_invalid="raise", **kwargs):
r"""
Creates geometries from the Well-Known Binary (WKB) representation.
The Well-Known Binary format is defined in the `OGC Simple Features
Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
Parameters
----------
geometry : str or array_like
The WKB byte object(s) to convert.
on_invalid : {"raise", "warn", "ignore"}, default "raise"
- raise: an exception will be raised if a WKB input geometry is invalid.
- warn: a warning will be raised and invalid WKB geometries will be
returned as ``None``.
- ignore: invalid WKB geometries will be returned as ``None`` without a warning.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from_wkb(b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?')
<POINT (1 1)>
"""
if not np.isscalar(on_invalid):
raise TypeError("on_invalid only accepts scalar values")
invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid))
# ensure the input has object dtype, to avoid numpy inferring it as a
# fixed-length string dtype (which removes trailing null bytes upon access
# of array elements)
geometry = np.asarray(geometry, dtype=object)
return lib.from_wkb(geometry, invalid_handler, **kwargs)
@requires_geos("3.10.1")
def from_geojson(geometry, on_invalid="raise", **kwargs):
"""Creates geometries from GeoJSON representations (strings).
If a GeoJSON is a FeatureCollection, it is read as a single geometry
(with type GEOMETRYCOLLECTION). This may be unpacked using the ``pygeos.get_parts``.
Properties are not read.
The GeoJSON format is defined in `RFC 7946 <https://geojson.org/>`__.
The following are currently unsupported:
- Three-dimensional geometries: the third dimension is ignored.
- Geometries having 'null' in the coordinates.
Parameters
----------
geometry : str, bytes or array_like
The GeoJSON string or byte object(s) to convert.
on_invalid : {"raise", "warn", "ignore"}, default "raise"
- raise: an exception will be raised if an input GeoJSON is invalid.
- warn: a warning will be raised and invalid input geometries will be
returned as ``None``.
- ignore: invalid input geometries will be returned as ``None`` without a warning.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
get_parts
Examples
--------
>>> from_geojson('{"type": "Point","coordinates": [1, 2]}')
<POINT (1 2)>
"""
# GEOS Tickets:
# - support 3D: https://trac.osgeo.org/geos/ticket/1141
# - handle null coordinates: https://trac.osgeo.org/geos/ticket/1142
if not np.isscalar(on_invalid):
raise TypeError("on_invalid only accepts scalar values")
invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid))
# ensure the input has object dtype, to avoid numpy inferring it as a
# fixed-length string dtype (which removes trailing null bytes upon access
# of array elements)
geometry = np.asarray(geometry, dtype=object)
return lib.from_geojson(geometry, invalid_handler, **kwargs)
@@ -0,0 +1,203 @@
from shapely import lib
from shapely.decorators import multithreading_enabled
from shapely.errors import UnsupportedGEOSVersionError
__all__ = [
"line_interpolate_point",
"line_locate_point",
"line_merge",
"shared_paths",
"shortest_line",
]
@multithreading_enabled
def line_interpolate_point(line, distance, normalized=False, **kwargs):
"""Returns a point interpolated at given distance on a line.
Parameters
----------
line : Geometry or array_like
For multilinestrings or geometrycollections, the first geometry is taken
and the rest is ignored. This function raises a TypeError for non-linear
geometries. For empty linear geometries, empty points are returned.
distance : float or array_like
Negative values measure distance from the end of the line. Out-of-range
values will be clipped to the line endings.
normalized : bool, default False
If True, the distance is a fraction of the total
line length instead of the absolute distance.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point
>>> line = LineString([(0, 2), (0, 10)])
>>> line_interpolate_point(line, 2)
<POINT (0 4)>
>>> line_interpolate_point(line, 100)
<POINT (0 10)>
>>> line_interpolate_point(line, -2)
<POINT (0 8)>
>>> line_interpolate_point(line, [0.25, -0.25], normalized=True).tolist()
[<POINT (0 4)>, <POINT (0 8)>]
>>> line_interpolate_point(LineString(), 1)
<POINT EMPTY>
"""
if normalized:
return lib.line_interpolate_point_normalized(line, distance)
else:
return lib.line_interpolate_point(line, distance)
@multithreading_enabled
def line_locate_point(line, other, normalized=False, **kwargs):
"""Returns the distance to the line origin of given point.
If given point does not intersect with the line, the point will first be
projected onto the line after which the distance is taken.
Parameters
----------
line : Geometry or array_like
point : Geometry or array_like
normalized : bool, default False
If True, the distance is a fraction of the total
line length instead of the absolute distance.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point
>>> line = LineString([(0, 2), (0, 10)])
>>> point = Point(4, 4)
>>> line_locate_point(line, point)
2.0
>>> line_locate_point(line, point, normalized=True)
0.25
>>> line_locate_point(line, Point(0, 18))
8.0
>>> line_locate_point(LineString(), point)
nan
"""
if normalized:
return lib.line_locate_point_normalized(line, other)
else:
return lib.line_locate_point(line, other)
@multithreading_enabled
def line_merge(line, directed=False, **kwargs):
"""Returns (Multi)LineStrings formed by combining the lines in a
MultiLineString.
Lines are joined together at their endpoints in case two lines are
intersecting. Lines are not joined when 3 or more lines are intersecting at
the endpoints. Line elements that cannot be joined are kept as is in the
resulting MultiLineString.
The direction of each merged LineString will be that of the majority of the
LineStrings from which it was derived. Except if ``directed=True`` is
specified, then the operation will not change the order of points within
lines and so only lines which can be joined with no change in direction
are merged.
Parameters
----------
line : Geometry or array_like
directed : bool, default False
Only combine lines if possible without changing point order.
Requires GEOS >= 3.11.0
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import MultiLineString
>>> line_merge(MultiLineString([[(0, 2), (0, 10)], [(0, 10), (5, 10)]]))
<LINESTRING (0 2, 0 10, 5 10)>
>>> line_merge(MultiLineString([[(0, 2), (0, 10)], [(0, 11), (5, 10)]]))
<MULTILINESTRING ((0 2, 0 10), (0 11, 5 10))>
>>> line_merge(MultiLineString())
<GEOMETRYCOLLECTION EMPTY>
>>> line_merge(MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]]))
<LINESTRING (1 0, 0 0, 3 0)>
>>> line_merge(MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]]), directed=True)
<MULTILINESTRING ((0 0, 1 0), (0 0, 3 0))>
"""
if directed:
if lib.geos_version < (3, 11, 0):
raise UnsupportedGEOSVersionError(
"'{}' requires at least GEOS {}.{}.{}.".format(
"line_merge", *(3, 11, 0)
)
)
return lib.line_merge_directed(line, **kwargs)
return lib.line_merge(line, **kwargs)
@multithreading_enabled
def shared_paths(a, b, **kwargs):
"""Returns the shared paths between geom1 and geom2.
Both geometries should be linestrings or arrays of linestrings.
A geometrycollection or array of geometrycollections is returned
with two elements in each geometrycollection. The first element is a
multilinestring containing shared paths with the same direction
for both inputs. The second element is a multilinestring containing
shared paths with the opposite direction for the two inputs.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
>>> line2 = LineString([(1, 0), (2, 0), (2, 1), (1, 1), (1, 0)])
>>> shared_paths(line1, line2).wkt
'GEOMETRYCOLLECTION (MULTILINESTRING EMPTY, MULTILINESTRING ((1 0, 1 1)))'
>>> line3 = LineString([(1, 1), (0, 1)])
>>> shared_paths(line1, line3).wkt
'GEOMETRYCOLLECTION (MULTILINESTRING ((1 1, 0 1)), MULTILINESTRING EMPTY)'
"""
return lib.shared_paths(a, b, **kwargs)
@multithreading_enabled
def shortest_line(a, b, **kwargs):
"""
Returns the shortest line between two geometries.
The resulting line consists of two points, representing the nearest
points between the geometry pair. The line always starts in the first
geometry `a` and ends in he second geometry `b`. The endpoints of the
line will not necessarily be existing vertices of the input geometries
`a` and `b`, but can also be a point along a line segment.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
prepare : improve performance by preparing ``a`` (the first argument) (for GEOS>=3.9)
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
>>> line2 = LineString([(0, 3), (3, 0), (5, 3)])
>>> shortest_line(line1, line2)
<LINESTRING (1 1, 1.5 1.5)>
"""
return lib.shortest_line(a, b, **kwargs)
@@ -0,0 +1,326 @@
import warnings
import numpy as np
from shapely import lib
from shapely.decorators import multithreading_enabled, requires_geos
__all__ = [
"area",
"distance",
"bounds",
"total_bounds",
"length",
"hausdorff_distance",
"frechet_distance",
"minimum_clearance",
"minimum_bounding_radius",
]
@multithreading_enabled
def area(geometry, **kwargs):
"""Computes the area of a (multi)polygon.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import MultiPolygon, Polygon
>>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
>>> area(polygon)
100.0
>>> area(MultiPolygon([polygon, Polygon([(10, 10), (10, 20), (20, 20), (20, 10), (10, 10)])]))
200.0
>>> area(Polygon())
0.0
>>> area(None)
nan
"""
return lib.area(geometry, **kwargs)
@multithreading_enabled
def distance(a, b, **kwargs):
"""Computes the Cartesian distance between two geometries.
Parameters
----------
a, b : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point, Polygon
>>> point = Point(0, 0)
>>> distance(Point(10, 0), point)
10.0
>>> distance(LineString([(1, 1), (1, -1)]), point)
1.0
>>> distance(Polygon([(3, 0), (5, 0), (5, 5), (3, 5), (3, 0)]), point)
3.0
>>> distance(Point(), point)
nan
>>> distance(None, point)
nan
"""
return lib.distance(a, b, **kwargs)
@multithreading_enabled
def bounds(geometry, **kwargs):
"""Computes the bounds (extent) of a geometry.
For each geometry these 4 numbers are returned: min x, min y, max x, max y.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, Point, Polygon
>>> bounds(Point(2, 3)).tolist()
[2.0, 3.0, 2.0, 3.0]
>>> bounds(LineString([(0, 0), (0, 2), (3, 2)])).tolist()
[0.0, 0.0, 3.0, 2.0]
>>> bounds(Polygon()).tolist()
[nan, nan, nan, nan]
>>> bounds(None).tolist()
[nan, nan, nan, nan]
"""
# We need to provide the `out` argument here for compatibility with
# numpy < 1.16. See https://github.com/numpy/numpy/issues/14949
geometry_arr = np.asarray(geometry, dtype=np.object_)
out = np.empty(geometry_arr.shape + (4,), dtype="float64")
return lib.bounds(geometry_arr, out=out, **kwargs)
def total_bounds(geometry, **kwargs):
"""Computes the total bounds (extent) of the geometry.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Returns
-------
numpy ndarray of [xmin, ymin, xmax, ymax]
Examples
--------
>>> from shapely import LineString, Point, Polygon
>>> total_bounds(Point(2, 3)).tolist()
[2.0, 3.0, 2.0, 3.0]
>>> total_bounds([Point(2, 3), Point(4, 5)]).tolist()
[2.0, 3.0, 4.0, 5.0]
>>> total_bounds([
... LineString([(0, 1), (0, 2), (3, 2)]),
... LineString([(4, 4), (4, 6), (6, 7)])
... ]).tolist()
[0.0, 1.0, 6.0, 7.0]
>>> total_bounds(Polygon()).tolist()
[nan, nan, nan, nan]
>>> total_bounds([Polygon(), Point(2, 3)]).tolist()
[2.0, 3.0, 2.0, 3.0]
>>> total_bounds(None).tolist()
[nan, nan, nan, nan]
"""
b = bounds(geometry, **kwargs)
if b.ndim == 1:
return b
with warnings.catch_warnings():
# ignore 'All-NaN slice encountered' warnings
warnings.simplefilter("ignore", RuntimeWarning)
return np.array(
[
np.nanmin(b[..., 0]),
np.nanmin(b[..., 1]),
np.nanmax(b[..., 2]),
np.nanmax(b[..., 3]),
]
)
@multithreading_enabled
def length(geometry, **kwargs):
"""Computes the length of a (multi)linestring or polygon perimeter.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString, MultiLineString, Polygon
>>> length(LineString([(0, 0), (0, 2), (3, 2)]))
5.0
>>> length(MultiLineString([
... LineString([(0, 0), (1, 0)]),
... LineString([(1, 0), (2, 0)])
... ]))
2.0
>>> length(Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]))
40.0
>>> length(LineString())
0.0
>>> length(None)
nan
"""
return lib.length(geometry, **kwargs)
@multithreading_enabled
def hausdorff_distance(a, b, densify=None, **kwargs):
"""Compute the discrete Hausdorff distance between two geometries.
The Hausdorff distance is a measure of similarity: it is the greatest
distance between any point in A and the closest point in B. The discrete
distance is an approximation of this metric: only vertices are considered.
The parameter 'densify' makes this approximation less coarse by splitting
the line segments between vertices before computing the distance.
Parameters
----------
a, b : Geometry or array_like
densify : float or array_like, optional
The value of densify is required to be between 0 and 1.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(130, 0), (0, 0), (0, 150)])
>>> line2 = LineString([(10, 10), (10, 150), (130, 10)])
>>> hausdorff_distance(line1, line2) # doctest: +ELLIPSIS
14.14...
>>> hausdorff_distance(line1, line2, densify=0.5)
70.0
>>> hausdorff_distance(line1, LineString())
nan
>>> hausdorff_distance(line1, None)
nan
"""
if densify is None:
return lib.hausdorff_distance(a, b, **kwargs)
else:
return lib.hausdorff_distance_densify(a, b, densify, **kwargs)
@requires_geos("3.7.0")
@multithreading_enabled
def frechet_distance(a, b, densify=None, **kwargs):
"""Compute the discrete Fréchet distance between two geometries.
The Fréchet distance is a measure of similarity: it is the greatest
distance between any point in A and the closest point in B. The discrete
distance is an approximation of this metric: only vertices are considered.
The parameter 'densify' makes this approximation less coarse by splitting
the line segments between vertices before computing the distance.
Fréchet distance sweep continuously along their respective curves
and the direction of curves is significant. This makes it a better measure
of similarity than Hausdorff distance for curve or surface matching.
Parameters
----------
a, b : Geometry or array_like
densify : float or array_like, optional
The value of densify is required to be between 0 and 1.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(0, 0), (100, 0)])
>>> line2 = LineString([(0, 0), (50, 50), (100, 0)])
>>> frechet_distance(line1, line2) # doctest: +ELLIPSIS
70.71...
>>> frechet_distance(line1, line2, densify=0.5)
50.0
>>> frechet_distance(line1, LineString())
nan
>>> frechet_distance(line1, None)
nan
"""
if densify is None:
return lib.frechet_distance(a, b, **kwargs)
return lib.frechet_distance_densify(a, b, densify, **kwargs)
@requires_geos("3.6.0")
@multithreading_enabled
def minimum_clearance(geometry, **kwargs):
"""Computes the Minimum Clearance distance.
A geometry's "minimum clearance" is the smallest distance by which
a vertex of the geometry could be moved to produce an invalid geometry.
If no minimum clearance exists for a geometry (for example, a single
point, or an empty geometry), infinity is returned.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import Polygon
>>> polygon = Polygon([(0, 0), (0, 10), (5, 6), (10, 10), (10, 0), (5, 4), (0, 0)])
>>> minimum_clearance(polygon)
2.0
>>> minimum_clearance(Polygon())
inf
>>> minimum_clearance(None)
nan
"""
return lib.minimum_clearance(geometry, **kwargs)
@requires_geos("3.8.0")
@multithreading_enabled
def minimum_bounding_radius(geometry, **kwargs):
"""Computes the radius of the minimum bounding circle that encloses an input geometry.
Parameters
----------
geometry : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
Examples
--------
>>> from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon
>>> minimum_bounding_radius(Polygon([(0, 5), (5, 10), (10, 5), (5, 0), (0, 5)]))
5.0
>>> minimum_bounding_radius(LineString([(1, 1), (1, 10)]))
4.5
>>> minimum_bounding_radius(MultiPoint([(2, 2), (4, 2)]))
1.0
>>> minimum_bounding_radius(Point(0, 1))
0.0
>>> minimum_bounding_radius(GeometryCollection())
0.0
See also
--------
minimum_bounding_circle
"""
return lib.minimum_bounding_radius(geometry, **kwargs)
@@ -0,0 +1,738 @@
"""Support for various GEOS geometry operations
"""
from warnings import warn
import shapely
from shapely.algorithms.polylabel import polylabel # noqa
from shapely.errors import GeometryTypeError, ShapelyDeprecationWarning
from shapely.geometry import (
GeometryCollection,
LineString,
MultiLineString,
MultiPoint,
Point,
Polygon,
shape,
)
from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
from shapely.geometry.polygon import orient as orient_
from shapely.prepared import prep
__all__ = [
"cascaded_union",
"linemerge",
"operator",
"polygonize",
"polygonize_full",
"transform",
"unary_union",
"triangulate",
"voronoi_diagram",
"split",
"nearest_points",
"validate",
"snap",
"shared_paths",
"clip_by_rect",
"orient",
"substring",
]
class CollectionOperator:
def shapeup(self, ob):
if isinstance(ob, BaseGeometry):
return ob
else:
try:
return shape(ob)
except (ValueError, AttributeError):
return LineString(ob)
def polygonize(self, lines):
"""Creates polygons from a source of lines
The source may be a MultiLineString, a sequence of LineString objects,
or a sequence of objects than can be adapted to LineStrings.
"""
source = getattr(lines, "geoms", None) or lines
try:
source = iter(source)
except TypeError:
source = [source]
finally:
obs = [self.shapeup(line) for line in source]
collection = shapely.polygonize(obs)
return collection.geoms
def polygonize_full(self, lines):
"""Creates polygons from a source of lines, returning the polygons
and leftover geometries.
The source may be a MultiLineString, a sequence of LineString objects,
or a sequence of objects than can be adapted to LineStrings.
Returns a tuple of objects: (polygons, cut edges, dangles, invalid ring
lines). Each are a geometry collection.
Dangles are edges which have one or both ends which are not incident on
another edge endpoint. Cut edges are connected at both ends but do not
form part of polygon. Invalid ring lines form rings which are invalid
(bowties, etc).
"""
source = getattr(lines, "geoms", None) or lines
try:
source = iter(source)
except TypeError:
source = [source]
finally:
obs = [self.shapeup(line) for line in source]
return shapely.polygonize_full(obs)
def linemerge(self, lines, directed=False):
"""Merges all connected lines from a source
The source may be a MultiLineString, a sequence of LineString objects,
or a sequence of objects than can be adapted to LineStrings. Returns a
LineString or MultiLineString when lines are not contiguous.
"""
source = None
if getattr(lines, "geom_type", None) == "MultiLineString":
source = lines
elif hasattr(lines, "geoms"):
# other Multi geometries
source = MultiLineString([ls.coords for ls in lines.geoms])
elif hasattr(lines, "__iter__"):
try:
source = MultiLineString([ls.coords for ls in lines])
except AttributeError:
source = MultiLineString(lines)
if source is None:
raise ValueError(f"Cannot linemerge {lines}")
return shapely.line_merge(source, directed=directed)
def cascaded_union(self, geoms):
"""Returns the union of a sequence of geometries
.. deprecated:: 1.8
This function was superseded by :meth:`unary_union`.
"""
warn(
"The 'cascaded_union()' function is deprecated. "
"Use 'unary_union()' instead.",
ShapelyDeprecationWarning,
stacklevel=2,
)
return shapely.union_all(geoms, axis=None)
def unary_union(self, geoms):
"""Returns the union of a sequence of geometries
Usually used to convert a collection into the smallest set of polygons
that cover the same area.
"""
return shapely.union_all(geoms, axis=None)
operator = CollectionOperator()
polygonize = operator.polygonize
polygonize_full = operator.polygonize_full
linemerge = operator.linemerge
cascaded_union = operator.cascaded_union
unary_union = operator.unary_union
def triangulate(geom, tolerance=0.0, edges=False):
"""Creates the Delaunay triangulation and returns a list of geometries
The source may be any geometry type. All vertices of the geometry will be
used as the points of the triangulation.
From the GEOS documentation:
tolerance is the snapping tolerance used to improve the robustness of
the triangulation computation. A tolerance of 0.0 specifies that no
snapping will take place.
If edges is False, a list of Polygons (triangles) will be returned.
Otherwise the list of LineString edges is returned.
"""
collection = shapely.delaunay_triangles(geom, tolerance=tolerance, only_edges=edges)
return [g for g in collection.geoms]
def voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False):
"""
Constructs a Voronoi Diagram [1] from the given geometry.
Returns a list of geometries.
Parameters
----------
geom: geometry
the input geometry whose vertices will be used to calculate
the final diagram.
envelope: geometry, None
clipping envelope for the returned diagram, automatically
determined if None. The diagram will be clipped to the larger
of this envelope or an envelope surrounding the sites.
tolerance: float, 0.0
sets the snapping tolerance used to improve the robustness
of the computation. A tolerance of 0.0 specifies that no
snapping will take place.
edges: bool, False
If False, return regions as polygons. Else, return only
edges e.g. LineStrings.
GEOS documentation can be found at [2]
Returns
-------
GeometryCollection
geometries representing the Voronoi regions.
Notes
-----
The tolerance `argument` can be finicky and is known to cause the
algorithm to fail in several cases. If you're using `tolerance`
and getting a failure, try removing it. The test cases in
tests/test_voronoi_diagram.py show more details.
References
----------
[1] https://en.wikipedia.org/wiki/Voronoi_diagram
[2] https://geos.osgeo.org/doxygen/geos__c_8h_source.html (line 730)
"""
try:
result = shapely.voronoi_polygons(
geom, tolerance=tolerance, extend_to=envelope, only_edges=edges
)
except shapely.GEOSException as err:
errstr = "Could not create Voronoi Diagram with the specified inputs "
errstr += f"({err!s})."
if tolerance:
errstr += " Try running again with default tolerance value."
raise ValueError(errstr) from err
if result.geom_type != "GeometryCollection":
return GeometryCollection([result])
return result
def validate(geom):
return shapely.is_valid_reason(geom)
def transform(func, geom):
"""Applies `func` to all coordinates of `geom` and returns a new
geometry of the same type from the transformed coordinates.
`func` maps x, y, and optionally z to output xp, yp, zp. The input
parameters may iterable types like lists or arrays or single values.
The output shall be of the same type. Scalars in, scalars out.
Lists in, lists out.
For example, here is an identity function applicable to both types
of input.
def id_func(x, y, z=None):
return tuple(filter(None, [x, y, z]))
g2 = transform(id_func, g1)
Using pyproj >= 2.1, this example will accurately project Shapely geometries:
import pyproj
wgs84 = pyproj.CRS('EPSG:4326')
utm = pyproj.CRS('EPSG:32618')
project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform
g2 = transform(project, g1)
Note that the always_xy kwarg is required here as Shapely geometries only support
X,Y coordinate ordering.
Lambda expressions such as the one in
g2 = transform(lambda x, y, z=None: (x+1.0, y+1.0), g1)
also satisfy the requirements for `func`.
"""
if geom.is_empty:
return geom
if geom.geom_type in ("Point", "LineString", "LinearRing", "Polygon"):
# First we try to apply func to x, y, z sequences. When func is
# optimized for sequences, this is the fastest, though zipping
# the results up to go back into the geometry constructors adds
# extra cost.
try:
if geom.geom_type in ("Point", "LineString", "LinearRing"):
return type(geom)(zip(*func(*zip(*geom.coords))))
elif geom.geom_type == "Polygon":
shell = type(geom.exterior)(zip(*func(*zip(*geom.exterior.coords))))
holes = list(
type(ring)(zip(*func(*zip(*ring.coords))))
for ring in geom.interiors
)
return type(geom)(shell, holes)
# A func that assumes x, y, z are single values will likely raise a
# TypeError, in which case we'll try again.
except TypeError:
if geom.geom_type in ("Point", "LineString", "LinearRing"):
return type(geom)([func(*c) for c in geom.coords])
elif geom.geom_type == "Polygon":
shell = type(geom.exterior)([func(*c) for c in geom.exterior.coords])
holes = list(
type(ring)([func(*c) for c in ring.coords])
for ring in geom.interiors
)
return type(geom)(shell, holes)
elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection":
return type(geom)([transform(func, part) for part in geom.geoms])
else:
raise GeometryTypeError(f"Type {geom.geom_type!r} not recognized")
def nearest_points(g1, g2):
"""Returns the calculated nearest points in the input geometries
The points are returned in the same order as the input geometries.
"""
seq = shapely.shortest_line(g1, g2)
if seq is None:
if g1.is_empty:
raise ValueError("The first input geometry is empty")
else:
raise ValueError("The second input geometry is empty")
p1 = shapely.get_point(seq, 0)
p2 = shapely.get_point(seq, 1)
return (p1, p2)
def snap(g1, g2, tolerance):
"""
Snaps an input geometry (g1) to reference (g2) geometry's vertices.
Parameters
----------
g1 : geometry
The first geometry
g2 : geometry
The second geometry
tolerance : float
The snapping tolerance
Refer to :func:`shapely.snap` for full documentation.
"""
return shapely.snap(g1, g2, tolerance)
def shared_paths(g1, g2):
"""Find paths shared between the two given lineal geometries
Returns a GeometryCollection with two elements:
- First element is a MultiLineString containing shared paths with the
same direction for both inputs.
- Second element is a MultiLineString containing shared paths with the
opposite direction for the two inputs.
Parameters
----------
g1 : geometry
The first geometry
g2 : geometry
The second geometry
"""
if not isinstance(g1, LineString):
raise GeometryTypeError("First geometry must be a LineString")
if not isinstance(g2, LineString):
raise GeometryTypeError("Second geometry must be a LineString")
return shapely.shared_paths(g1, g2)
class SplitOp:
@staticmethod
def _split_polygon_with_line(poly, splitter):
"""Split a Polygon with a LineString"""
if not isinstance(poly, Polygon):
raise GeometryTypeError("First argument must be a Polygon")
if not isinstance(splitter, LineString):
raise GeometryTypeError("Second argument must be a LineString")
union = poly.boundary.union(splitter)
# greatly improves split performance for big geometries with many
# holes (the following contains checks) with minimal overhead
# for common cases
poly = prep(poly)
# some polygonized geometries may be holes, we do not want them
# that's why we test if the original polygon (poly) contains
# an inner point of polygonized geometry (pg)
return [
pg for pg in polygonize(union) if poly.contains(pg.representative_point())
]
@staticmethod
def _split_line_with_line(line, splitter):
"""Split a LineString with another (Multi)LineString or (Multi)Polygon"""
# if splitter is a polygon, pick it's boundary
if splitter.geom_type in ("Polygon", "MultiPolygon"):
splitter = splitter.boundary
if not isinstance(line, LineString):
raise GeometryTypeError("First argument must be a LineString")
if not isinstance(splitter, LineString) and not isinstance(
splitter, MultiLineString
):
raise GeometryTypeError(
"Second argument must be either a LineString or a MultiLineString"
)
# | s\l | Interior | Boundary | Exterior |
# |----------|----------|----------|----------|
# | Interior | 0 or F | * | * | At least one of these two must be 0
# | Boundary | 0 or F | * | * | So either '0********' or '[0F]**0*****'
# | Exterior | * | * | * | No overlapping interiors ('1********')
relation = splitter.relate(line)
if relation[0] == "1":
# The lines overlap at some segment (linear intersection of interiors)
raise ValueError("Input geometry segment overlaps with the splitter.")
elif relation[0] == "0" or relation[3] == "0":
# The splitter crosses or touches the line's interior --> return multilinestring from the split
return line.difference(splitter)
else:
# The splitter does not cross or touch the line's interior --> return collection with identity line
return [line]
@staticmethod
def _split_line_with_point(line, splitter):
"""Split a LineString with a Point"""
if not isinstance(line, LineString):
raise GeometryTypeError("First argument must be a LineString")
if not isinstance(splitter, Point):
raise GeometryTypeError("Second argument must be a Point")
# check if point is in the interior of the line
if not line.relate_pattern(splitter, "0********"):
# point not on line interior --> return collection with single identity line
# (REASONING: Returning a list with the input line reference and creating a
# GeometryCollection at the general split function prevents unnecessary copying
# of linestrings in multipoint splitting function)
return [line]
elif line.coords[0] == splitter.coords[0]:
# if line is a closed ring the previous test doesn't behave as desired
return [line]
# point is on line, get the distance from the first point on line
distance_on_line = line.project(splitter)
coords = list(line.coords)
# split the line at the point and create two new lines
current_position = 0.0
for i in range(len(coords) - 1):
point1 = coords[i]
point2 = coords[i + 1]
dx = point1[0] - point2[0]
dy = point1[1] - point2[1]
segment_length = (dx**2 + dy**2) ** 0.5
current_position += segment_length
if distance_on_line == current_position:
# splitter is exactly on a vertex
return [LineString(coords[: i + 2]), LineString(coords[i + 1 :])]
elif distance_on_line < current_position:
# splitter is between two vertices
return [
LineString(coords[: i + 1] + [splitter.coords[0]]),
LineString([splitter.coords[0]] + coords[i + 1 :]),
]
return [line]
@staticmethod
def _split_line_with_multipoint(line, splitter):
"""Split a LineString with a MultiPoint"""
if not isinstance(line, LineString):
raise GeometryTypeError("First argument must be a LineString")
if not isinstance(splitter, MultiPoint):
raise GeometryTypeError("Second argument must be a MultiPoint")
chunks = [line]
for pt in splitter.geoms:
new_chunks = []
for chunk in filter(lambda x: not x.is_empty, chunks):
# add the newly split 2 lines or the same line if not split
new_chunks.extend(SplitOp._split_line_with_point(chunk, pt))
chunks = new_chunks
return chunks
@staticmethod
def split(geom, splitter):
"""
Splits a geometry by another geometry and returns a collection of geometries. This function is the theoretical
opposite of the union of the split geometry parts. If the splitter does not split the geometry, a collection
with a single geometry equal to the input geometry is returned.
The function supports:
- Splitting a (Multi)LineString by a (Multi)Point or (Multi)LineString or (Multi)Polygon
- Splitting a (Multi)Polygon by a LineString
It may be convenient to snap the splitter with low tolerance to the geometry. For example in the case
of splitting a line by a point, the point must be exactly on the line, for the line to be correctly split.
When splitting a line by a polygon, the boundary of the polygon is used for the operation.
When splitting a line by another line, a ValueError is raised if the two overlap at some segment.
Parameters
----------
geom : geometry
The geometry to be split
splitter : geometry
The geometry that will split the input geom
Example
-------
>>> pt = Point((1, 1))
>>> line = LineString([(0,0), (2,2)])
>>> result = split(line, pt)
>>> result.wkt
'GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), LINESTRING (1 1, 2 2))'
"""
if geom.geom_type in ("MultiLineString", "MultiPolygon"):
return GeometryCollection(
[i for part in geom.geoms for i in SplitOp.split(part, splitter).geoms]
)
elif geom.geom_type == "LineString":
if splitter.geom_type in (
"LineString",
"MultiLineString",
"Polygon",
"MultiPolygon",
):
split_func = SplitOp._split_line_with_line
elif splitter.geom_type == "Point":
split_func = SplitOp._split_line_with_point
elif splitter.geom_type == "MultiPoint":
split_func = SplitOp._split_line_with_multipoint
else:
raise GeometryTypeError(
f"Splitting a LineString with a {splitter.geom_type} is not supported"
)
elif geom.geom_type == "Polygon":
if splitter.geom_type == "LineString":
split_func = SplitOp._split_polygon_with_line
else:
raise GeometryTypeError(
f"Splitting a Polygon with a {splitter.geom_type} is not supported"
)
else:
raise GeometryTypeError(
f"Splitting {geom.geom_type} geometry is not supported"
)
return GeometryCollection(split_func(geom, splitter))
split = SplitOp.split
def substring(geom, start_dist, end_dist, normalized=False):
"""Return a line segment between specified distances along a LineString
Negative distance values are taken as measured in the reverse
direction from the end of the geometry. Out-of-range index
values are handled by clamping them to the valid range of values.
If the start distance equals the end distance, a Point is returned.
If the start distance is actually beyond the end distance, then the
reversed substring is returned such that the start distance is
at the first coordinate.
Parameters
----------
geom : LineString
The geometry to get a substring of.
start_dist : float
The distance along `geom` of the start of the substring.
end_dist : float
The distance along `geom` of the end of the substring.
normalized : bool, False
Whether the distance parameters are interpreted as a
fraction of the geometry's length.
Returns
-------
Union[Point, LineString]
The substring between `start_dist` and `end_dist` or a Point
if they are at the same location.
Raises
------
TypeError
If `geom` is not a LineString.
Examples
--------
>>> from shapely.geometry import LineString
>>> from shapely.ops import substring
>>> ls = LineString((i, 0) for i in range(6))
>>> ls.wkt
'LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)'
>>> substring(ls, start_dist=1, end_dist=3).wkt
'LINESTRING (1 0, 2 0, 3 0)'
>>> substring(ls, start_dist=3, end_dist=1).wkt
'LINESTRING (3 0, 2 0, 1 0)'
>>> substring(ls, start_dist=1, end_dist=-3).wkt
'LINESTRING (1 0, 2 0)'
>>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True).wkt
'LINESTRING (1 0, 2 0)'
Returning a `Point` when `start_dist` and `end_dist` are at the
same location.
>>> substring(ls, 2.5, -2.5).wkt
'POINT (2.5 0)'
"""
if not isinstance(geom, LineString):
raise GeometryTypeError(
"Can only calculate a substring of LineString geometries. "
f"A {geom.geom_type} was provided."
)
# Filter out cases in which to return a point
if start_dist == end_dist:
return geom.interpolate(start_dist, normalized)
elif not normalized and start_dist >= geom.length and end_dist >= geom.length:
return geom.interpolate(geom.length, normalized)
elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length:
return geom.interpolate(0, normalized)
elif normalized and start_dist >= 1 and end_dist >= 1:
return geom.interpolate(1, normalized)
elif normalized and -start_dist >= 1 and -end_dist >= 1:
return geom.interpolate(0, normalized)
if normalized:
start_dist *= geom.length
end_dist *= geom.length
# Filter out cases where distances meet at a middle point from opposite ends.
if start_dist < 0 < end_dist and abs(start_dist) + end_dist == geom.length:
return geom.interpolate(end_dist)
elif end_dist < 0 < start_dist and abs(end_dist) + start_dist == geom.length:
return geom.interpolate(start_dist)
start_point = geom.interpolate(start_dist)
end_point = geom.interpolate(end_dist)
if start_dist < 0:
start_dist = geom.length + start_dist # Values may still be negative,
if end_dist < 0: # but only in the out-of-range
end_dist = geom.length + end_dist # sense, not the wrap-around sense.
reverse = start_dist > end_dist
if reverse:
start_dist, end_dist = end_dist, start_dist
if start_dist < 0:
start_dist = 0 # to avoid duplicating the first vertex
if reverse:
vertex_list = [tuple(*end_point.coords)]
else:
vertex_list = [tuple(*start_point.coords)]
coords = list(geom.coords)
current_distance = 0
for p1, p2 in zip(coords, coords[1:]):
if start_dist < current_distance < end_dist:
vertex_list.append(p1)
elif current_distance >= end_dist:
break
current_distance += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5
if reverse:
vertex_list.append(tuple(*start_point.coords))
# reverse direction result
vertex_list = reversed(vertex_list)
else:
vertex_list.append(tuple(*end_point.coords))
return LineString(vertex_list)
def clip_by_rect(geom, xmin, ymin, xmax, ymax):
"""Returns the portion of a geometry within a rectangle
The geometry is clipped in a fast but possibly dirty way. The output is
not guaranteed to be valid. No exceptions will be raised for topological
errors.
Parameters
----------
geom : geometry
The geometry to be clipped
xmin : float
Minimum x value of the rectangle
ymin : float
Minimum y value of the rectangle
xmax : float
Maximum x value of the rectangle
ymax : float
Maximum y value of the rectangle
Notes
-----
Requires GEOS >= 3.5.0
New in 1.7.
"""
if geom.is_empty:
return geom
return shapely.clip_by_rect(geom, xmin, ymin, xmax, ymax)
def orient(geom, sign=1.0):
"""A properly oriented copy of the given geometry.
The signed area of the result will have the given sign. A sign of
1.0 means that the coordinates of the product's exterior rings will
be oriented counter-clockwise.
Parameters
----------
geom : Geometry
The original geometry. May be a Polygon, MultiPolygon, or
GeometryCollection.
sign : float, optional.
The sign of the result's signed area.
Returns
-------
Geometry
"""
if isinstance(geom, BaseMultipartGeometry):
return geom.__class__(
list(
map(
lambda geom: orient(geom, sign),
geom.geoms,
)
)
)
if isinstance(geom, (Polygon,)):
return orient_(geom, sign)
return geom
@@ -0,0 +1,217 @@
"""
Plot single geometries using Matplotlib.
Note: this module is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
"""
import numpy as np
import shapely
def _default_ax():
import matplotlib.pyplot as plt
ax = plt.gca()
ax.grid(True)
ax.set_aspect("equal")
return ax
def _path_from_polygon(polygon):
from matplotlib.path import Path
if isinstance(polygon, shapely.MultiPolygon):
return Path.make_compound_path(
*[_path_from_polygon(poly) for poly in polygon.geoms]
)
else:
return Path.make_compound_path(
Path(np.asarray(polygon.exterior.coords)[:, :2]),
*[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors],
)
def patch_from_polygon(polygon, **kwargs):
"""
Gets a Matplotlib patch from a (Multi)Polygon.
Note: this function is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
----------
polygon : shapely.Polygon or shapely.MultiPolygon
**kwargs
Additional keyword arguments passed to the matplotlib Patch.
Returns
-------
Matplotlib artist (PathPatch)
"""
from matplotlib.patches import PathPatch
return PathPatch(_path_from_polygon(polygon), **kwargs)
def plot_polygon(
polygon,
ax=None,
add_points=True,
color=None,
facecolor=None,
edgecolor=None,
linewidth=None,
**kwargs
):
"""
Plot a (Multi)Polygon.
Note: this function is experimental, and mainly targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
----------
polygon : shapely.Polygon or shapely.MultiPolygon
ax : matplotlib Axes, default None
The axes on which to draw the plot. If not specified, will get the
current active axes or create a new figure.
add_points : bool, default True
If True, also plot the coordinates (vertices) as points.
color : matplotlib color specification
Color for both the polygon fill (face) and boundary (edge). By default,
the fill is using an alpha of 0.3. You can specify `facecolor` and
`edgecolor` separately for greater control.
facecolor : matplotlib color specification
Color for the polygon fill.
edgecolor : matplotlib color specification
Color for the polygon boundary.
linewidth : float
The line width for the polygon boundary.
**kwargs
Additional keyword arguments passed to the matplotlib Patch.
Returns
-------
Matplotlib artist (PathPatch), if `add_points` is false.
A tuple of Matplotlib artists (PathPatch, Line2D), if `add_points` is true.
"""
from matplotlib import colors
if ax is None:
ax = _default_ax()
if color is None:
color = "C0"
color = colors.to_rgba(color)
if facecolor is None:
facecolor = list(color)
facecolor[-1] = 0.3
facecolor = tuple(facecolor)
if edgecolor is None:
edgecolor = color
patch = patch_from_polygon(
polygon, facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, **kwargs
)
ax.add_patch(patch)
ax.autoscale_view()
if add_points:
line = plot_points(polygon, ax=ax, color=color)
return patch, line
return patch
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 targeting (interactive)
exploration, debugging and illustration purposes.
Parameters
----------
line : shapely.LineString or shapely.LinearRing
ax : matplotlib Axes, default None
The axes on which to draw the plot. If not specified, will get the
current active axes or create a new figure.
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 points.
linewidth : float, default 2
The line width for the polygon boundary.
**kwargs
Additional keyword arguments passed to the matplotlib Patch.
Returns
-------
Matplotlib artist (PathPatch)
"""
from matplotlib.patches import PathPatch
from matplotlib.path import Path
if ax is None:
ax = _default_ax()
if color is None:
color = "C0"
if isinstance(line, shapely.MultiLineString):
path = Path.make_compound_path(
*[Path(np.asarray(mline.coords)[:, :2]) for mline in line.geoms]
)
else:
path = Path(np.asarray(line.coords)[:, :2])
patch = PathPatch(
path, facecolor="none", edgecolor=color, linewidth=linewidth, **kwargs
)
ax.add_patch(patch)
ax.autoscale_view()
if add_points:
line = plot_points(line, ax=ax, color=color)
return patch, line
return patch
def plot_points(geom, ax=None, color=None, marker="o", **kwargs):
"""
Plot a Point/MultiPoint or the vertices of any other geometry type.
Parameters
----------
geom : shapely.Geometry
Any shapely Geometry object, from which all vertices are extracted
and plotted.
ax : matplotlib Axes, default None
The axes on which to draw the plot. If not specified, will get the
current active axes or create a new figure.
color : matplotlib color specification
Color for the filled points. You can use `markeredgecolor` and
`markeredgecolor` to have different edge and fill colors.
marker : str, default "o"
The matplotlib marker for the points.
**kwargs
Additional keyword arguments passed to matplotlib `plot` (Line2D).
Returns
-------
Matplotlib artist (Line2D)
"""
if ax is None:
ax = _default_ax()
coords = shapely.get_coordinates(geom)
(line,) = ax.plot(
coords[:, 0], coords[:, 1], linestyle="", marker=marker, color=color, **kwargs
)
return line
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
"""
Support for GEOS prepared geometry operations.
"""
from pickle import PicklingError
import shapely
class PreparedGeometry:
"""
A geometry prepared for efficient comparison to a set of other geometries.
Example:
>>> from shapely.geometry import Point, Polygon
>>> triangle = Polygon([(0.0, 0.0), (1.0, 1.0), (1.0, -1.0)])
>>> p = prep(triangle)
>>> p.intersects(Point(0.5, 0.5))
True
"""
def __init__(self, context):
if isinstance(context, PreparedGeometry):
self.context = context.context
else:
shapely.prepare(context)
self.context = context
self.prepared = True
def contains(self, other):
"""Returns True if the geometry contains the other, else False"""
return self.context.contains(other)
def contains_properly(self, other):
"""Returns True if the geometry properly contains the other, else False"""
# TODO temporary hack until shapely exposes contains properly as predicate function
from shapely import STRtree
tree = STRtree([other])
idx = tree.query(self.context, predicate="contains_properly")
return bool(len(idx))
def covers(self, other):
"""Returns True if the geometry covers the other, else False"""
return self.context.covers(other)
def crosses(self, other):
"""Returns True if the geometries cross, else False"""
return self.context.crosses(other)
def disjoint(self, other):
"""Returns True if geometries are disjoint, else False"""
return self.context.disjoint(other)
def intersects(self, other):
"""Returns True if geometries intersect, else False"""
return self.context.intersects(other)
def overlaps(self, other):
"""Returns True if geometries overlap, else False"""
return self.context.overlaps(other)
def touches(self, other):
"""Returns True if geometries touch, else False"""
return self.context.touches(other)
def within(self, other):
"""Returns True if geometry is within the other, else False"""
return self.context.within(other)
def __reduce__(self):
raise PicklingError("Prepared geometries cannot be pickled.")
def prep(ob):
"""Creates and returns a prepared geometric object."""
return PreparedGeometry(ob)
@@ -0,0 +1,509 @@
import numpy as np
from shapely import GeometryType, lib
from shapely.decorators import multithreading_enabled, requires_geos
from shapely.errors import UnsupportedGEOSVersionError
__all__ = [
"difference",
"intersection",
"intersection_all",
"symmetric_difference",
"symmetric_difference_all",
"unary_union",
"union",
"union_all",
"coverage_union",
"coverage_union_all",
]
@multithreading_enabled
def difference(a, b, grid_size=None, **kwargs):
"""Returns the part of geometry A that does not intersect with geometry B.
If grid_size is nonzero, input coordinates will be snapped to a precision
grid of that size and resulting coordinates will be snapped to that same
grid. If 0, this operation will use double precision coordinates. If None,
the highest precision of the inputs will be used, which may be previously
set using set_precision. Note: returned geometry does not have precision
set unless specified previously by set_precision.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
grid_size : float, optional
Precision grid size; requires GEOS >= 3.9.0. Will use the highest
precision of the inputs by default.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
set_precision
Examples
--------
>>> from shapely import box, LineString, normalize, Polygon
>>> line = LineString([(0, 0), (2, 2)])
>>> difference(line, LineString([(1, 1), (3, 3)]))
<LINESTRING (0 0, 1 1)>
>>> difference(line, LineString())
<LINESTRING (0 0, 2 2)>
>>> difference(line, None) is None
True
>>> box1 = box(0, 0, 2, 2)
>>> box2 = box(1, 1, 3, 3)
>>> normalize(difference(box1, box2))
<POLYGON ((0 0, 0 2, 1 2, 1 1, 2 1, 2 0, 0 0))>
>>> box1 = box(0.1, 0.2, 2.1, 2.1)
>>> difference(box1, box2, grid_size=1)
<POLYGON ((2 0, 0 0, 0 2, 1 2, 1 1, 2 1, 2 0))>
"""
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
raise UnsupportedGEOSVersionError(
"grid_size parameter requires GEOS >= 3.9.0"
)
if not np.isscalar(grid_size):
raise ValueError("grid_size parameter only accepts scalar values")
return lib.difference_prec(a, b, grid_size, **kwargs)
return lib.difference(a, b, **kwargs)
@multithreading_enabled
def intersection(a, b, grid_size=None, **kwargs):
"""Returns the geometry that is shared between input geometries.
If grid_size is nonzero, input coordinates will be snapped to a precision
grid of that size and resulting coordinates will be snapped to that same
grid. If 0, this operation will use double precision coordinates. If None,
the highest precision of the inputs will be used, which may be previously
set using set_precision. Note: returned geometry does not have precision
set unless specified previously by set_precision.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
grid_size : float, optional
Precision grid size; requires GEOS >= 3.9.0. Will use the highest
precision of the inputs by default.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
intersection_all
set_precision
Examples
--------
>>> from shapely import box, LineString, normalize, Polygon
>>> line = LineString([(0, 0), (2, 2)])
>>> intersection(line, LineString([(1, 1), (3, 3)]))
<LINESTRING (1 1, 2 2)>
>>> box1 = box(0, 0, 2, 2)
>>> box2 = box(1, 1, 3, 3)
>>> normalize(intersection(box1, box2))
<POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1))>
>>> box1 = box(0.1, 0.2, 2.1, 2.1)
>>> intersection(box1, box2, grid_size=1)
<POLYGON ((2 2, 2 1, 1 1, 1 2, 2 2))>
"""
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
raise UnsupportedGEOSVersionError(
"grid_size parameter requires GEOS >= 3.9.0"
)
if not np.isscalar(grid_size):
raise ValueError("grid_size parameter only accepts scalar values")
return lib.intersection_prec(a, b, grid_size, **kwargs)
return lib.intersection(a, b, **kwargs)
@multithreading_enabled
def intersection_all(geometries, axis=None, **kwargs):
"""Returns the intersection of multiple geometries.
This function ignores None values when other Geometry elements are present.
If all elements of the given axis are None, an empty GeometryCollection is
returned.
Parameters
----------
geometries : array_like
axis : int, optional
Axis along which the operation is performed. The default (None)
performs the operation over all axes, returning a scalar value.
Axis may be negative, in which case it counts from the last to the
first axis.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
intersection
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(0, 0), (2, 2)])
>>> line2 = LineString([(1, 1), (3, 3)])
>>> intersection_all([line1, line2])
<LINESTRING (1 1, 2 2)>
>>> intersection_all([[line1, line2, None]], axis=1).tolist()
[<LINESTRING (1 1, 2 2)>]
>>> intersection_all([line1, None])
<LINESTRING (0 0, 2 2)>
"""
geometries = np.asarray(geometries)
if axis is None:
geometries = geometries.ravel()
else:
geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim)
return lib.intersection_all(geometries, **kwargs)
@multithreading_enabled
def symmetric_difference(a, b, grid_size=None, **kwargs):
"""Returns the geometry that represents the portions of input geometries
that do not intersect.
If grid_size is nonzero, input coordinates will be snapped to a precision
grid of that size and resulting coordinates will be snapped to that same
grid. If 0, this operation will use double precision coordinates. If None,
the highest precision of the inputs will be used, which may be previously
set using set_precision. Note: returned geometry does not have precision
set unless specified previously by set_precision.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
grid_size : float, optional
Precision grid size; requires GEOS >= 3.9.0. Will use the highest
precision of the inputs by default.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
symmetric_difference_all
set_precision
Examples
--------
>>> from shapely import box, LineString, normalize
>>> line = LineString([(0, 0), (2, 2)])
>>> symmetric_difference(line, LineString([(1, 1), (3, 3)]))
<MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))>
>>> box1 = box(0, 0, 2, 2)
>>> box2 = box(1, 1, 3, 3)
>>> normalize(symmetric_difference(box1, box2))
<MULTIPOLYGON (((1 2, 1 3, 3 3, 3 1, 2 1, 2 2, 1 2)), ((0 0, 0 2, 1 2, 1 1, ...>
>>> box1 = box(0.1, 0.2, 2.1, 2.1)
>>> symmetric_difference(box1, box2, grid_size=1)
<MULTIPOLYGON (((2 0, 0 0, 0 2, 1 2, 1 1, 2 1, 2 0)), ((2 2, 1 2, 1 3, 3 3, ...>
"""
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
raise UnsupportedGEOSVersionError(
"grid_size parameter requires GEOS >= 3.9.0"
)
if not np.isscalar(grid_size):
raise ValueError("grid_size parameter only accepts scalar values")
return lib.symmetric_difference_prec(a, b, grid_size, **kwargs)
return lib.symmetric_difference(a, b, **kwargs)
@multithreading_enabled
def symmetric_difference_all(geometries, axis=None, **kwargs):
"""Returns the symmetric difference of multiple geometries.
This function ignores None values when other Geometry elements are present.
If all elements of the given axis are None an empty GeometryCollection is
returned.
Parameters
----------
geometries : array_like
axis : int, optional
Axis along which the operation is performed. The default (None)
performs the operation over all axes, returning a scalar value.
Axis may be negative, in which case it counts from the last to the
first axis.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
symmetric_difference
Examples
--------
>>> from shapely import LineString
>>> line1 = LineString([(0, 0), (2, 2)])
>>> line2 = LineString([(1, 1), (3, 3)])
>>> symmetric_difference_all([line1, line2])
<MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))>
>>> symmetric_difference_all([[line1, line2, None]], axis=1).tolist()
[<MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))>]
>>> symmetric_difference_all([line1, None])
<LINESTRING (0 0, 2 2)>
>>> symmetric_difference_all([None, None])
<GEOMETRYCOLLECTION EMPTY>
"""
geometries = np.asarray(geometries)
if axis is None:
geometries = geometries.ravel()
else:
geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim)
return lib.symmetric_difference_all(geometries, **kwargs)
@multithreading_enabled
def union(a, b, grid_size=None, **kwargs):
"""Merges geometries into one.
If grid_size is nonzero, input coordinates will be snapped to a precision
grid of that size and resulting coordinates will be snapped to that same
grid. If 0, this operation will use double precision coordinates. If None,
the highest precision of the inputs will be used, which may be previously
set using set_precision. Note: returned geometry does not have precision
set unless specified previously by set_precision.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
grid_size : float, optional
Precision grid size; requires GEOS >= 3.9.0. Will use the highest
precision of the inputs by default.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
union_all
set_precision
Examples
--------
>>> from shapely import box, LineString, normalize
>>> line = LineString([(0, 0), (2, 2)])
>>> union(line, LineString([(2, 2), (3, 3)]))
<MULTILINESTRING ((0 0, 2 2), (2 2, 3 3))>
>>> union(line, None) is None
True
>>> box1 = box(0, 0, 2, 2)
>>> box2 = box(1, 1, 3, 3)
>>> normalize(union(box1, box2))
<POLYGON ((0 0, 0 2, 1 2, 1 3, 3 3, 3 1, 2 1, 2 0, 0 0))>
>>> box1 = box(0.1, 0.2, 2.1, 2.1)
>>> union(box1, box2, grid_size=1)
<POLYGON ((2 0, 0 0, 0 2, 1 2, 1 3, 3 3, 3 1, 2 1, 2 0))>
"""
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
raise UnsupportedGEOSVersionError(
"grid_size parameter requires GEOS >= 3.9.0"
)
if not np.isscalar(grid_size):
raise ValueError("grid_size parameter only accepts scalar values")
return lib.union_prec(a, b, grid_size, **kwargs)
return lib.union(a, b, **kwargs)
@multithreading_enabled
def union_all(geometries, grid_size=None, axis=None, **kwargs):
"""Returns the union of multiple geometries.
This function ignores None values when other Geometry elements are present.
If all elements of the given axis are None an empty GeometryCollection is
returned.
If grid_size is nonzero, input coordinates will be snapped to a precision
grid of that size and resulting coordinates will be snapped to that same
grid. If 0, this operation will use double precision coordinates. If None,
the highest precision of the inputs will be used, which may be previously
set using set_precision. Note: returned geometry does not have precision
set unless specified previously by set_precision.
`unary_union` is an alias of `union_all`.
Parameters
----------
geometries : array_like
grid_size : float, optional
Precision grid size; requires GEOS >= 3.9.0. Will use the highest
precision of the inputs by default.
axis : int, optional
Axis along which the operation is performed. The default (None)
performs the operation over all axes, returning a scalar value.
Axis may be negative, in which case it counts from the last to the
first axis.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
union
set_precision
Examples
--------
>>> from shapely import box, LineString, normalize, Point
>>> line1 = LineString([(0, 0), (2, 2)])
>>> line2 = LineString([(2, 2), (3, 3)])
>>> union_all([line1, line2])
<MULTILINESTRING ((0 0, 2 2), (2 2, 3 3))>
>>> union_all([[line1, line2, None]], axis=1).tolist()
[<MULTILINESTRING ((0 0, 2 2), (2 2, 3 3))>]
>>> box1 = box(0, 0, 2, 2)
>>> box2 = box(1, 1, 3, 3)
>>> normalize(union_all([box1, box2]))
<POLYGON ((0 0, 0 2, 1 2, 1 3, 3 3, 3 1, 2 1, 2 0, 0 0))>
>>> box1 = box(0.1, 0.2, 2.1, 2.1)
>>> union_all([box1, box2], grid_size=1)
<POLYGON ((2 0, 0 0, 0 2, 1 2, 1 3, 3 3, 3 1, 2 1, 2 0))>
>>> union_all([None, Point(0, 1)])
<POINT (0 1)>
>>> union_all([None, None])
<GEOMETRYCOLLECTION EMPTY>
>>> union_all([])
<GEOMETRYCOLLECTION EMPTY>
"""
# for union_all, GEOS provides an efficient route through first creating
# GeometryCollections
# first roll the aggregation axis backwards
geometries = np.asarray(geometries)
if axis is None:
geometries = geometries.ravel()
else:
geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim)
# create_collection acts on the inner axis
collections = lib.create_collection(
geometries, np.intc(GeometryType.GEOMETRYCOLLECTION)
)
if grid_size is not None:
if lib.geos_version < (3, 9, 0):
raise UnsupportedGEOSVersionError(
"grid_size parameter requires GEOS >= 3.9.0"
)
if not np.isscalar(grid_size):
raise ValueError("grid_size parameter only accepts scalar values")
return lib.unary_union_prec(collections, grid_size, **kwargs)
return lib.unary_union(collections, **kwargs)
unary_union = union_all
@requires_geos("3.8.0")
@multithreading_enabled
def coverage_union(a, b, **kwargs):
"""Merges multiple polygons into one. This is an optimized version of
union which assumes the polygons to be non-overlapping.
Parameters
----------
a : Geometry or array_like
b : Geometry or array_like
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
coverage_union_all
Examples
--------
>>> from shapely import normalize, Polygon
>>> polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
>>> normalize(coverage_union(polygon, Polygon([(1, 0), (1, 1), (2, 1), (2, 0), (1, 0)])))
<POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))>
Union with None returns same polygon
>>> normalize(coverage_union(polygon, None))
<POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))>
"""
return coverage_union_all([a, b], **kwargs)
@requires_geos("3.8.0")
@multithreading_enabled
def coverage_union_all(geometries, axis=None, **kwargs):
"""Returns the union of multiple polygons of a geometry collection.
This is an optimized version of union which assumes the polygons
to be non-overlapping.
This function ignores None values when other Geometry elements are present.
If all elements of the given axis are None, an empty MultiPolygon is
returned.
Parameters
----------
geometries : array_like
axis : int, optional
Axis along which the operation is performed. The default (None)
performs the operation over all axes, returning a scalar value.
Axis may be negative, in which case it counts from the last to the
first axis.
**kwargs
See :ref:`NumPy ufunc docs <ufuncs.kwargs>` for other keyword arguments.
See also
--------
coverage_union
Examples
--------
>>> from shapely import normalize, Polygon
>>> polygon_1 = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
>>> polygon_2 = Polygon([(1, 0), (1, 1), (2, 1), (2, 0), (1, 0)])
>>> normalize(coverage_union_all([polygon_1, polygon_2]))
<POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))>
>>> normalize(coverage_union_all([polygon_1, None]))
<POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))>
>>> normalize(coverage_union_all([None, None]))
<MULTIPOLYGON EMPTY>
"""
# coverage union in GEOS works over GeometryCollections
# first roll the aggregation axis backwards
geometries = np.asarray(geometries)
if axis is None:
geometries = geometries.ravel()
else:
geometries = np.rollaxis(
np.asarray(geometries), axis=axis, start=geometries.ndim
)
# create_collection acts on the inner axis
collections = lib.create_collection(
geometries, np.intc(GeometryType.GEOMETRYCOLLECTION)
)
return lib.coverage_union(collections, **kwargs)
@@ -0,0 +1,36 @@
import warnings
__all__ = ["available", "enable", "disable", "enabled"]
available = True
enabled = True
_MSG = (
"This function has no longer any effect, and will be removed in a "
"future release. Starting with Shapely 2.0, equivalent speedups are "
"always available"
)
def enable():
"""
This function has no longer any effect, and will be removed in a future
release.
Previously, this function enabled cython-based speedups. Starting with
Shapely 2.0, equivalent speedups are available in every installation.
"""
warnings.warn(_MSG, DeprecationWarning, stacklevel=2)
def disable():
"""
This function has no longer any effect, and will be removed in a future
release.
Previously, this function enabled cython-based speedups. Starting with
Shapely 2.0, equivalent speedups are available in every installation.
"""
warnings.warn(_MSG, DeprecationWarning, stacklevel=2)
@@ -0,0 +1,544 @@
from typing import Any, Iterable, Union
import numpy as np
from shapely import lib
from shapely._enum import ParamEnum
from shapely.decorators import requires_geos, UnsupportedGEOSVersionError
from shapely.geometry.base import BaseGeometry
from shapely.predicates import is_empty, is_missing
__all__ = ["STRtree"]
class BinaryPredicate(ParamEnum):
"""The enumeration of GEOS binary predicates types"""
intersects = 1
within = 2
contains = 3
overlaps = 4
crosses = 5
touches = 6
covers = 7
covered_by = 8
contains_properly = 9
class STRtree:
"""
A query-only R-tree spatial index created using the
Sort-Tile-Recursive (STR) [1]_ algorithm.
The tree indexes the bounding boxes of each geometry. The tree is
constructed directly at initialization and nodes cannot be added or
removed after it has been created.
All operations return indices of the input geometries. These indices
can be used to index into anything associated with the input geometries,
including the input geometries themselves, or custom items stored in
another object of the same length as the geometries.
Bounding boxes limited to two dimensions and are axis-aligned (equivalent to
the ``bounds`` property of a geometry); any Z values present in geometries
are ignored for purposes of indexing within the tree.
Any mixture of geometry types may be stored in the tree.
Note: the tree is more efficient for querying when there are fewer
geometries that have overlapping bounding boxes and where there is greater
similarity between the outer boundary of a geometry and its bounding box.
For example, a MultiPolygon composed of widely-spaced individual Polygons
will have a large overall bounding box compared to the boundaries of its
individual Polygons, and the bounding box may also potentially overlap many
other geometries within the tree. This means that the resulting tree may be
less efficient to query than a tree constructed from individual Polygons.
Parameters
----------
geoms : sequence
A sequence of geometry objects.
node_capacity : int, default 10
The maximum number of child nodes per parent node in the tree.
References
----------
.. [1] Leutenegger, Scott T.; Edgington, Jeffrey M.; Lopez, Mario A.
(February 1997). "STR: A Simple and Efficient Algorithm for
R-Tree Packing".
https://ia600900.us.archive.org/27/items/nasa_techdoc_19970016975/19970016975.pdf
"""
def __init__(
self,
geoms: Iterable[BaseGeometry],
node_capacity: int = 10,
):
# Keep references to geoms in a copied array so that this array is not
# modified while the tree depends on it remaining the same
self._geometries = np.array(geoms, dtype=np.object_, copy=True)
# initialize GEOS STRtree
self._tree = lib.STRtree(self.geometries, node_capacity)
def __len__(self):
return self._tree.count
def __reduce__(self):
return (STRtree, (self.geometries,))
@property
def geometries(self):
"""
Geometries stored in the tree in the order used to construct the tree.
The order of this array corresponds to the tree indices returned by
other STRtree methods.
Do not attempt to modify items in the returned array.
Returns
-------
ndarray of Geometry objects
"""
return self._geometries
def query(self, geometry, predicate=None, distance=None):
"""
Return the integer indices of all combinations of each input geometry
and tree geometries where the bounding box of each input geometry
intersects the bounding box of a tree geometry.
If the input geometry is a scalar, this returns an array of shape (n, ) with
the indices of the matching tree geometries. If the input geometry is an
array_like, this returns an array with shape (2,n) where the subarrays
correspond to the indices of the input geometries and indices of the
tree geometries associated with each. To generate an array of pairs of
input geometry index and tree geometry index, simply transpose the
result.
If a predicate is provided, the tree geometries are first queried based
on the bounding box of the input geometry and then are further filtered
to those that meet the predicate when comparing the input geometry to
the tree geometry:
predicate(geometry, tree_geometry)
The 'dwithin' predicate requires GEOS >= 3.10.
Bounding boxes are limited to two dimensions and are axis-aligned
(equivalent to the ``bounds`` property of a geometry); any Z values
present in input geometries are ignored when querying the tree.
Any input geometry that is None or empty will never match geometries in
the tree.
Parameters
----------
geometry : Geometry or array_like
Input geometries to query the tree and filter results using the
optional predicate.
predicate : {None, 'intersects', 'within', 'contains', 'overlaps', 'crosses',\
'touches', 'covers', 'covered_by', 'contains_properly', 'dwithin'}, optional
The predicate to use for testing geometries from the tree
that are within the input geometry's bounding box.
distance : number or array_like, optional
Distances around each input geometry within which to query the tree
for the 'dwithin' predicate. If array_like, shape must be
broadcastable to shape of geometry. Required if predicate='dwithin'.
Returns
-------
ndarray with shape (n,) if geometry is a scalar
Contains tree geometry indices.
OR
ndarray with shape (2, n) if geometry is an array_like
The first subarray contains input geometry indices.
The second subarray contains tree geometry indices.
Examples
--------
>>> from shapely import box, Point
>>> import numpy as np
>>> points = [Point(0, 0), Point(1, 1), Point(2,2), Point(3, 3)]
>>> tree = STRtree(points)
Query the tree using a scalar geometry:
>>> indices = tree.query(box(0, 0, 1, 1))
>>> indices.tolist()
[0, 1]
Query using an array of geometries:
>>> boxes = np.array([box(0, 0, 1, 1), box(2, 2, 3, 3)])
>>> arr_indices = tree.query(boxes)
>>> arr_indices.tolist()
[[0, 0, 1, 1], [0, 1, 2, 3]]
Or transpose to get all pairs of input and tree indices:
>>> arr_indices.T.tolist()
[[0, 0], [0, 1], [1, 2], [1, 3]]
Retrieve the tree geometries by results of query:
>>> tree.geometries.take(indices).tolist()
[<POINT (0 0)>, <POINT (1 1)>]
Retrieve all pairs of input and tree geometries:
>>> np.array([boxes.take(arr_indices[0]),\
tree.geometries.take(arr_indices[1])]).T.tolist()
[[<POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0))>, <POINT (0 0)>],
[<POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0))>, <POINT (1 1)>],
[<POLYGON ((3 2, 3 3, 2 3, 2 2, 3 2))>, <POINT (2 2)>],
[<POLYGON ((3 2, 3 3, 2 3, 2 2, 3 2))>, <POINT (3 3)>]]
Query using a predicate:
>>> tree = STRtree([box(0, 0, 0.5, 0.5), box(0.5, 0.5, 1, 1), box(1, 1, 2, 2)])
>>> tree.query(box(0, 0, 1, 1), predicate="contains").tolist()
[0, 1]
>>> tree.query(Point(0.75, 0.75), predicate="dwithin", distance=0.5).tolist()
[0, 1, 2]
>>> tree.query(boxes, predicate="contains").tolist()
[[0, 0], [0, 1]]
>>> tree.query(boxes, predicate="dwithin", distance=0.5).tolist()
[[0, 0, 0, 1], [0, 1, 2, 2]]
Retrieve custom items associated with tree geometries (records can
be in whatever data structure so long as geometries and custom data
can be extracted into arrays of the same length and order):
>>> records = [
... {"geometry": Point(0, 0), "value": "A"},
... {"geometry": Point(2, 2), "value": "B"}
... ]
>>> tree = STRtree([record["geometry"] for record in records])
>>> items = np.array([record["value"] for record in records])
>>> items.take(tree.query(box(0, 0, 1, 1))).tolist()
['A']
Notes
-----
In the context of a spatial join, input geometries are the "left"
geometries that determine the order of the results, and tree geometries
are "right" geometries that are joined against the left geometries. This
effectively performs an inner join, where only those combinations of
geometries that can be joined based on overlapping bounding boxes or
optional predicate are returned.
"""
geometry = np.asarray(geometry)
is_scalar = False
if geometry.ndim == 0:
geometry = np.expand_dims(geometry, 0)
is_scalar = True
if predicate is None:
indices = self._tree.query(geometry, 0)
return indices[1] if is_scalar else indices
# Requires GEOS >= 3.10
elif predicate == "dwithin":
if lib.geos_version < (3, 10, 0):
raise UnsupportedGEOSVersionError(
"dwithin predicate requires GEOS >= 3.10"
)
if distance is None:
raise ValueError(
"distance parameter must be provided for dwithin predicate"
)
distance = np.asarray(distance, dtype="float64")
if distance.ndim > 1:
raise ValueError("Distance array should be one dimensional")
try:
distance = np.broadcast_to(distance, geometry.shape)
except ValueError:
raise ValueError("Could not broadcast distance to match geometry")
indices = self._tree.dwithin(geometry, distance)
return indices[1] if is_scalar else indices
predicate = BinaryPredicate.get_value(predicate)
indices = self._tree.query(geometry, predicate)
return indices[1] if is_scalar else indices
@requires_geos("3.6.0")
def nearest(self, geometry) -> Union[Any, None]:
"""
Return the index of the nearest geometry in the tree for each input
geometry based on distance within two-dimensional Cartesian space.
This distance will be 0 when input geometries intersect tree geometries.
If there are multiple equidistant or intersected geometries in the tree,
only a single result is returned for each input geometry, based on the
order that tree geometries are visited; this order may be
nondeterministic.
If any input geometry is None or empty, an error is raised. Any Z
values present in input geometries are ignored when finding nearest
tree geometries.
Parameters
----------
geometry : Geometry or array_like
Input geometries to query the tree.
Returns
-------
scalar or ndarray
Indices of geometries in tree. Return value will have the same shape
as the input.
None is returned if this index is empty. This may change in
version 2.0.
See also
--------
query_nearest: returns all equidistant geometries, exclusive geometries, \
and optional distances
Examples
--------
>>> from shapely.geometry import Point
>>> tree = STRtree([Point(i, i) for i in range(10)])
Query the tree for nearest using a scalar geometry:
>>> index = tree.nearest(Point(2.2, 2.2))
>>> index
2
>>> tree.geometries.take(index)
<POINT (2 2)>
Query the tree for nearest using an array of geometries:
>>> indices = tree.nearest([Point(2.2, 2.2), Point(4.4, 4.4)])
>>> indices.tolist()
[2, 4]
>>> tree.geometries.take(indices).tolist()
[<POINT (2 2)>, <POINT (4 4)>]
Nearest only return one object if there are multiple equidistant results:
>>> tree = STRtree ([Point(0, 0), Point(0, 0)])
>>> tree.nearest(Point(0, 0))
0
"""
if self._tree.count == 0:
return None
geometry_arr = np.asarray(geometry, dtype=object)
if is_missing(geometry_arr).any() or is_empty(geometry_arr).any():
raise ValueError(
"Cannot determine nearest geometry for empty geometry or "
"missing value (None)."
)
# _tree.nearest returns ndarray with shape (2, 1) -> index in input
# geometries and index into tree geometries
indices = self._tree.nearest(np.atleast_1d(geometry_arr))[1]
if geometry_arr.ndim == 0:
return indices[0]
else:
return indices
@requires_geos("3.6.0")
def query_nearest(
self,
geometry,
max_distance=None,
return_distance=False,
exclusive=False,
all_matches=True,
):
"""Return the index of the nearest geometries in the tree for each input
geometry based on distance within two-dimensional Cartesian space.
This distance will be 0 when input geometries intersect tree geometries.
If there are multiple equidistant or intersected geometries in tree and
`all_matches` is True (the default), all matching tree geometries are
returned; otherwise only the first matching tree geometry is returned.
Tree indices are returned in the order they are visited for each input
geometry and may not be in ascending index order; no meaningful order is
implied.
The max_distance used to search for nearest items in the tree may have a
significant impact on performance by reducing the number of input
geometries that are evaluated for nearest items in the tree. Only those
input geometries with at least one tree geometry within +/- max_distance
beyond their envelope will be evaluated. However, using a large
max_distance may have a negative performance impact because many tree
geometries will be queried for each input geometry.
The distance, if returned, will be 0 for any intersected geometries in
the tree.
Any geometry that is None or empty in the input geometries is omitted
from the output. Any Z values present in input geometries are ignored
when finding nearest tree geometries.
Parameters
----------
geometry : Geometry or array_like
Input geometries to query the tree.
max_distance : float, optional
Maximum distance within which to query for nearest items in tree.
Must be greater than 0.
return_distance : bool, default False
If True, will return distances in addition to indices.
exclusive : bool, default False
If True, the nearest tree geometries that are equal to the input
geometry will not be returned.
all_matches : bool, default True
If True, all equidistant and intersected geometries will be returned
for each input geometry.
If False, only the first nearest geometry will be returned.
Returns
-------
tree indices or tuple of (tree indices, distances) if geometry is a scalar
indices is an ndarray of shape (n, ) and distances (if present) an
ndarray of shape (n, )
OR
indices or tuple of (indices, distances)
indices is an ndarray of shape (2,n) and distances (if present) an
ndarray of shape (n).
The first subarray of indices contains input geometry indices.
The second subarray of indices contains tree geometry indices.
See also
--------
nearest: returns singular nearest geometry for each input
Examples
--------
>>> import numpy as np
>>> from shapely import box, Point
>>> points = [Point(0, 0), Point(1, 1), Point(2,2), Point(3, 3)]
>>> tree = STRtree(points)
Find the nearest tree geometries to a scalar geometry:
>>> indices = tree.query_nearest(Point(0.25, 0.25))
>>> indices.tolist()
[0]
Retrieve the tree geometries by results of query:
>>> tree.geometries.take(indices).tolist()
[<POINT (0 0)>]
Find the nearest tree geometries to an array of geometries:
>>> query_points = np.array([Point(2.25, 2.25), Point(1, 1)])
>>> arr_indices = tree.query_nearest(query_points)
>>> arr_indices.tolist()
[[0, 1], [2, 1]]
Or transpose to get all pairs of input and tree indices:
>>> arr_indices.T.tolist()
[[0, 2], [1, 1]]
Retrieve all pairs of input and tree geometries:
>>> list(zip(query_points.take(arr_indices[0]), tree.geometries.take(arr_indices[1])))
[(<POINT (2.25 2.25)>, <POINT (2 2)>), (<POINT (1 1)>, <POINT (1 1)>)]
All intersecting geometries in the tree are returned by default:
>>> tree.query_nearest(box(1,1,3,3)).tolist()
[1, 2, 3]
Set all_matches to False to to return a single match per input geometry:
>>> tree.query_nearest(box(1,1,3,3), all_matches=False).tolist()
[1]
Return the distance to each nearest tree geometry:
>>> index, distance = tree.query_nearest(Point(0.5, 0.5), return_distance=True)
>>> index.tolist()
[0, 1]
>>> distance.round(4).tolist()
[0.7071, 0.7071]
Return the distance for each input and nearest tree geometry for an array
of geometries:
>>> indices, distance = tree.query_nearest([Point(0.5, 0.5), Point(1, 1)], return_distance=True)
>>> indices.tolist()
[[0, 0, 1], [0, 1, 1]]
>>> distance.round(4).tolist()
[0.7071, 0.7071, 0.0]
Retrieve custom items associated with tree geometries (records can
be in whatever data structure so long as geometries and custom data
can be extracted into arrays of the same length and order):
>>> records = [
... {"geometry": Point(0, 0), "value": "A"},
... {"geometry": Point(2, 2), "value": "B"}
... ]
>>> tree = STRtree([record["geometry"] for record in records])
>>> items = np.array([record["value"] for record in records])
>>> items.take(tree.query_nearest(Point(0.5, 0.5))).tolist()
['A']
"""
geometry = np.asarray(geometry, dtype=object)
is_scalar = False
if geometry.ndim == 0:
geometry = np.expand_dims(geometry, 0)
is_scalar = True
if max_distance is not None:
if not np.isscalar(max_distance):
raise ValueError("max_distance parameter only accepts scalar values")
if max_distance <= 0:
raise ValueError("max_distance must be greater than 0")
# a distance of 0 means no max_distance is used
max_distance = max_distance or 0
if not np.isscalar(exclusive):
raise ValueError("exclusive parameter only accepts scalar values")
if exclusive not in {True, False}:
raise ValueError("exclusive parameter must be boolean")
if not np.isscalar(all_matches):
raise ValueError("all_matches parameter only accepts scalar values")
if all_matches not in {True, False}:
raise ValueError("all_matches parameter must be boolean")
results = self._tree.query_nearest(
geometry, max_distance, exclusive, all_matches
)
# output indices are shape (n, )
if is_scalar:
if not return_distance:
return results[0][1]
else:
return (results[0][1], results[1])
# output indices are shape (2, n)
if not return_distance:
return results[0]
return results
@@ -0,0 +1,204 @@
from functools import partial
import numpy as np
import shapely
__all__ = ["assert_geometries_equal"]
def _equals_exact_with_ndim(x, y, tolerance):
dimension_equals = shapely.get_coordinate_dimension(
x
) == shapely.get_coordinate_dimension(y)
with np.errstate(invalid="ignore"):
# Suppress 'invalid value encountered in equals_exact' with nan coordinates
geometry_equals = shapely.equals_exact(x, y, tolerance=tolerance)
return dimension_equals & geometry_equals
def _replace_nan(arr):
return np.where(np.isnan(arr), 0.0, arr)
def _assert_nan_coords_same(x, y, tolerance, err_msg, verbose):
x, y = np.broadcast_arrays(x, y)
x_coords = shapely.get_coordinates(x, include_z=True)
y_coords = shapely.get_coordinates(y, include_z=True)
# Check the shapes (condition is copied from numpy test_array_equal)
if x_coords.shape != y_coords.shape:
return False
# Check NaN positional equality
x_id = np.isnan(x_coords)
y_id = np.isnan(y_coords)
if not (x_id == y_id).all():
msg = build_err_msg(
[x, y],
err_msg + "\nx and y nan coordinate location mismatch:",
verbose=verbose,
)
raise AssertionError(msg)
# If this passed, replace NaN with a number to be able to use equals_exact
x_no_nan = shapely.transform(x, _replace_nan, include_z=True)
y_no_nan = shapely.transform(y, _replace_nan, include_z=True)
return _equals_exact_with_ndim(x_no_nan, y_no_nan, tolerance=tolerance)
def _assert_none_same(x, y, err_msg, verbose):
x_id = shapely.is_missing(x)
y_id = shapely.is_missing(y)
if not (x_id == y_id).all():
msg = build_err_msg(
[x, y],
err_msg + "\nx and y None location mismatch:",
verbose=verbose,
)
raise AssertionError(msg)
# If there is a scalar, then here we know the array has the same
# flag as it everywhere, so we should return the scalar flag.
if x.ndim == 0:
return bool(x_id)
elif y.ndim == 0:
return bool(y_id)
else:
return y_id
def assert_geometries_equal(
x,
y,
tolerance=1e-7,
equal_none=True,
equal_nan=True,
normalize=False,
err_msg="",
verbose=True,
):
"""Raises an AssertionError if two geometry array_like objects are not equal.
Given two array_like objects, check that the shape is equal and all elements of
these objects are equal. An exception is raised at shape mismatch or conflicting
values. In contrast to the standard usage in shapely, no assertion is raised if
both objects have NaNs/Nones in the same positions.
Parameters
----------
x : Geometry or array_like
y : Geometry or array_like
equal_none : bool, default True
Whether to consider None elements equal to other None elements.
equal_nan : bool, default True
Whether to consider nan coordinates as equal to other nan coordinates.
normalize : bool, default False
Whether to normalize geometries prior to comparison.
err_msg : str, optional
The error message to be printed in case of failure.
verbose : bool, optional
If True, the conflicting values are appended to the error message.
"""
__tracebackhide__ = True # Hide traceback for py.test
if normalize:
x = shapely.normalize(x)
y = shapely.normalize(y)
x = np.asarray(x)
y = np.asarray(y)
is_scalar = x.ndim == 0 or y.ndim == 0
# Check the shapes (condition is copied from numpy test_array_equal)
if not (is_scalar or x.shape == y.shape):
msg = build_err_msg(
[x, y],
err_msg + f"\n(shapes {x.shape}, {y.shape} mismatch)",
verbose=verbose,
)
raise AssertionError(msg)
flagged = False
if equal_none:
flagged = _assert_none_same(x, y, err_msg, verbose)
if not np.isscalar(flagged):
x, y = x[~flagged], y[~flagged]
# Only do the comparison if actual values are left
if x.size == 0:
return
elif flagged:
# no sense doing comparison if everything is flagged.
return
is_equal = _equals_exact_with_ndim(x, y, tolerance=tolerance)
if is_scalar and not np.isscalar(is_equal):
is_equal = bool(is_equal[0])
if np.all(is_equal):
return
elif not equal_nan:
msg = build_err_msg(
[x, y],
err_msg + f"\nNot equal to tolerance {tolerance:g}",
verbose=verbose,
)
raise AssertionError(msg)
# Optionally refine failing elements if NaN should be considered equal
if not np.isscalar(is_equal):
x, y = x[~is_equal], y[~is_equal]
# Only do the NaN check if actual values are left
if x.size == 0:
return
elif is_equal:
# no sense in checking for NaN if everything is equal.
return
is_equal = _assert_nan_coords_same(x, y, tolerance, err_msg, verbose)
if not np.all(is_equal):
msg = build_err_msg(
[x, y],
err_msg + f"\nNot equal to tolerance {tolerance:g}",
verbose=verbose,
)
raise AssertionError(msg)
## BELOW A COPY FROM numpy.testing._private.utils (numpy version 1.20.2)
def build_err_msg(
arrays,
err_msg,
header="Geometries are not equal:",
verbose=True,
names=("x", "y"),
precision=8,
):
msg = ["\n" + header]
if err_msg:
if err_msg.find("\n") == -1 and len(err_msg) < 79 - len(header):
msg = [msg[0] + " " + err_msg]
else:
msg.append(err_msg)
if verbose:
for i, a in enumerate(arrays):
if isinstance(a, np.ndarray):
# precision argument is only needed if the objects are ndarrays
r_func = partial(np.array_repr, precision=precision)
else:
r_func = repr
try:
r = r_func(a)
except Exception as exc:
r = f"[repr failed for <{type(a).__name__}>: {exc}]"
if r.count("\n") > 3:
r = "\n".join(r.splitlines()[:3])
r += "..."
msg.append(f" {names[i]}: {r}")
return "\n".join(msg)
@@ -0,0 +1,125 @@
from contextlib import contextmanager
import numpy as np
import pytest
import shapely
shapely20_todo = pytest.mark.xfail(
strict=False, reason="Not yet implemented for Shapely 2.0"
)
point_polygon_testdata = (
shapely.points(np.arange(6), np.arange(6)),
shapely.box(2, 2, 4, 4),
)
point = shapely.Point(2, 3)
line_string = shapely.LineString([(0, 0), (1, 0), (1, 1)])
linear_ring = shapely.LinearRing([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
polygon = shapely.Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
multi_point = shapely.MultiPoint([(0, 0), (1, 2)])
multi_line_string = shapely.MultiLineString([[(0, 0), (1, 2)]])
multi_polygon = shapely.multipolygons(
[
[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)],
[(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)],
]
)
geometry_collection = shapely.GeometryCollection(
[shapely.Point(51, -1), shapely.LineString([(52, -1), (49, 2)])]
)
point_z = shapely.Point(2, 3, 4)
line_string_z = shapely.LineString([(0, 0, 4), (1, 0, 4), (1, 1, 4)])
polygon_z = shapely.Polygon([(0, 0, 4), (2, 0, 4), (2, 2, 4), (0, 2, 4), (0, 0, 4)])
geometry_collection_z = shapely.GeometryCollection([point_z, line_string_z])
polygon_with_hole = shapely.Polygon(
[(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]],
)
empty_point = shapely.from_wkt("POINT EMPTY")
empty_point_z = shapely.from_wkt("POINT Z EMPTY")
empty_line_string = shapely.from_wkt("LINESTRING EMPTY")
empty_line_string_z = shapely.from_wkt("LINESTRING Z EMPTY")
empty_polygon = shapely.from_wkt("POLYGON EMPTY")
empty = shapely.from_wkt("GEOMETRYCOLLECTION EMPTY")
multi_point_z = shapely.MultiPoint([(0, 0, 4), (1, 2, 4)])
multi_line_string_z = shapely.MultiLineString([[(0, 0, 4), (1, 2, 4)]])
multi_polygon_z = shapely.multipolygons(
[
[(0, 0, 4), (1, 0, 4), (1, 1, 4), (0, 1, 4), (0, 0, 4)],
[(2.1, 2.1, 4), (2.2, 2.1, 4), (2.2, 2.2, 4), (2.1, 2.2, 4), (2.1, 2.1, 4)],
]
)
polygon_with_hole_z = shapely.Polygon(
[(0, 0, 4), (0, 10, 4), (10, 10, 4), (10, 0, 4), (0, 0, 4)],
holes=[[(2, 2, 4), (2, 4, 4), (4, 4, 4), (4, 2, 4), (2, 2, 4)]],
)
all_types = (
point,
line_string,
linear_ring,
polygon,
multi_point,
multi_line_string,
multi_polygon,
geometry_collection,
empty,
)
all_types_z = (
point_z,
line_string_z,
polygon_z,
multi_point_z,
multi_line_string_z,
multi_polygon_z,
polygon_with_hole_z,
geometry_collection_z,
empty_point_z,
empty_line_string_z,
)
@contextmanager
def ignore_invalid(condition=True):
if condition:
with np.errstate(invalid="ignore"):
yield
else:
yield
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
@@ -0,0 +1,82 @@
import numpy as np
import pytest
from shapely import GeometryCollection, LineString, Point, wkt
from shapely.geometry import shape
@pytest.fixture()
def geometrycollection_geojson():
return {
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": (0, 3, 0)},
{"type": "LineString", "coordinates": ((2, 0), (1, 0))},
],
}
@pytest.mark.parametrize(
"geom",
[
GeometryCollection(),
shape({"type": "GeometryCollection", "geometries": []}),
wkt.loads("GEOMETRYCOLLECTION EMPTY"),
],
)
def test_empty(geom):
assert geom.geom_type == "GeometryCollection"
assert geom.is_empty
assert len(geom.geoms) == 0
assert list(geom.geoms) == []
def test_empty_subgeoms():
geom = GeometryCollection([Point(), LineString()])
assert geom.geom_type == "GeometryCollection"
assert geom.is_empty
assert len(geom.geoms) == 2
assert list(geom.geoms) == [Point(), LineString()]
def test_child_with_deleted_parent():
# test that we can remove a collection while keeping
# children around
a = LineString([(0, 0), (1, 1), (1, 2), (2, 2)])
b = LineString([(0, 0), (1, 1), (2, 1), (2, 2)])
collection = a.intersection(b)
child = collection.geoms[0]
# delete parent of child
del collection
# access geometry, this should not seg fault as 1.2.15 did
assert child.wkt is not None
def test_from_geojson(geometrycollection_geojson):
geom = shape(geometrycollection_geojson)
assert geom.geom_type == "GeometryCollection"
assert len(geom.geoms) == 2
geom_types = [g.geom_type for g in geom.geoms]
assert "Point" in geom_types
assert "LineString" in geom_types
def test_geointerface(geometrycollection_geojson):
geom = shape(geometrycollection_geojson)
assert geom.__geo_interface__ == geometrycollection_geojson
def test_len_raises(geometrycollection_geojson):
geom = shape(geometrycollection_geojson)
with pytest.raises(TypeError):
len(geom)
def test_numpy_object_array():
geom = GeometryCollection([LineString([(0, 0), (1, 1)])])
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
@@ -0,0 +1,102 @@
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:
"""
Shapely assumes contiguous C-order float64 data for internal ops.
Data should be converted to contiguous float64 if numpy exists.
c9a0707 broke this a little bit.
"""
def test_data_promotion(self):
coords = np.array([[12, 34], [56, 78]], dtype=np.float32)
processed_coords = np.array(LineString(coords).coords)
assert coords.tolist() == processed_coords.tolist()
def test_data_destriding(self):
coords = np.array([[12, 34], [56, 78]], dtype=np.float32)
# Easy way to introduce striding: reverse list order
processed_coords = np.array(LineString(coords[::-1]).coords)
assert coords[::-1].tolist() == processed_coords.tolist()
class TestCoordsGetItem:
def test_index_2d_coords(self):
c = [(float(x), float(-x)) for x in range(4)]
g = LineString(c)
for i in range(-4, 4):
assert g.coords[i] == c[i]
with pytest.raises(IndexError):
g.coords[4]
with pytest.raises(IndexError):
g.coords[-5]
def test_index_3d_coords(self):
c = [(float(x), float(-x), float(x * 2)) for x in range(4)]
g = LineString(c)
for i in range(-4, 4):
assert g.coords[i] == c[i]
with pytest.raises(IndexError):
g.coords[4]
with pytest.raises(IndexError):
g.coords[-5]
def test_index_coords_misc(self):
g = LineString() # empty
with pytest.raises(IndexError):
g.coords[0]
with pytest.raises(TypeError):
g.coords[0.0]
def test_slice_2d_coords(self):
c = [(float(x), float(-x)) for x in range(4)]
g = LineString(c)
assert g.coords[1:] == c[1:]
assert g.coords[:-1] == c[:-1]
assert g.coords[::-1] == c[::-1]
assert g.coords[::2] == c[::2]
assert g.coords[:4] == c[:4]
assert g.coords[4:] == c[4:] == []
def test_slice_3d_coords(self):
c = [(float(x), float(-x), float(x * 2)) for x in range(4)]
g = LineString(c)
assert g.coords[1:] == c[1:]
assert g.coords[:-1] == c[:-1]
assert g.coords[::-1] == c[::-1]
assert g.coords[::2] == c[::2]
assert g.coords[:4] == c[:4]
assert g.coords[4:] == c[4:] == []
class TestXY:
"""New geometry/coordseq method 'xy' makes numpy interop easier"""
def test_arrays(self):
x, y = LineString([(0, 0), (1, 1)]).xy
assert len(x) == 2
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)
@@ -0,0 +1,117 @@
from decimal import Decimal
import pytest
from shapely import (
GeometryCollection,
LinearRing,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
items2d = [
[(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)],
[(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)],
]
items2d_mixed = [
[
(Decimal(0.0), Decimal(0.0)),
(Decimal(70.0), 120.0),
(140.0, Decimal(0.0)),
(0.0, 0.0),
],
[
(Decimal(60.0), Decimal(80.0)),
(Decimal(80.0), 80.0),
(70.0, Decimal(60.0)),
(60.0, 80.0),
],
]
items2d_decimal = [
[
(Decimal(0.0), Decimal(0.0)),
(Decimal(70.0), Decimal(120.0)),
(Decimal(140.0), Decimal(0.0)),
(Decimal(0.0), Decimal(0.0)),
],
[
(Decimal(60.0), Decimal(80.0)),
(Decimal(80.0), Decimal(80.0)),
(Decimal(70.0), Decimal(60.0)),
(Decimal(60.0), Decimal(80.0)),
],
]
items3d = [
[(0.0, 0.0, 1), (70.0, 120.0, 2), (140.0, 0.0, 3), (0.0, 0.0, 1)],
[(60.0, 80.0, 1), (80.0, 80.0, 2), (70.0, 60.0, 3), (60.0, 80.0, 1)],
]
items3d_mixed = [
[
(Decimal(0.0), Decimal(0.0), Decimal(1)),
(Decimal(70.0), 120.0, Decimal(2)),
(140.0, Decimal(0.0), 3),
(0.0, 0.0, 1),
],
[
(Decimal(60.0), Decimal(80.0), Decimal(1)),
(Decimal(80.0), 80.0, 2),
(70.0, Decimal(60.0), Decimal(3)),
(60.0, 80.0, 1),
],
]
items3d_decimal = [
[
(Decimal(0.0), Decimal(0.0), Decimal(1)),
(Decimal(70.0), Decimal(120.0), Decimal(2)),
(Decimal(140.0), Decimal(0.0), Decimal(3)),
(Decimal(0.0), Decimal(0.0), Decimal(1)),
],
[
(Decimal(60.0), Decimal(80.0), Decimal(1)),
(Decimal(80.0), Decimal(80.0), Decimal(2)),
(Decimal(70.0), Decimal(60.0), Decimal(3)),
(Decimal(60.0), Decimal(80.0), Decimal(1)),
],
]
all_geoms = [
[
Point(items[0][0]),
Point(*items[0][0]),
MultiPoint(items[0]),
LinearRing(items[0]),
LineString(items[0]),
MultiLineString(items),
Polygon(items[0]),
MultiPolygon(
[
Polygon(items[1]),
Polygon(items[0], holes=items[1:]),
]
),
GeometryCollection([Point(items[0][0]), Polygon(items[0])]),
]
for items in [
items2d,
items2d_mixed,
items2d_decimal,
items3d,
items3d_mixed,
items3d_decimal,
]
]
@pytest.mark.parametrize("geoms", list(zip(*all_geoms)))
def test_decimal(geoms):
assert geoms[0] == geoms[1] == geoms[2]
assert geoms[3] == geoms[4] == geoms[5]
@@ -0,0 +1,98 @@
import math
import numpy as np
import pytest
from shapely import (
GeometryCollection,
LinearRing,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
from shapely.geometry import mapping, shape
from shapely.geometry.base import BaseGeometry, EmptyGeometry
def empty_generator():
return iter([])
class TestEmptiness:
def test_empty_class(self):
with pytest.warns(FutureWarning):
g = EmptyGeometry()
assert g.is_empty
def test_empty_base(self):
with pytest.warns(FutureWarning):
g = BaseGeometry()
assert g.is_empty
def test_empty_point(self):
assert Point().is_empty
def test_empty_multipoint(self):
assert MultiPoint().is_empty
def test_empty_geometry_collection(self):
assert GeometryCollection().is_empty
def test_empty_linestring(self):
assert LineString().is_empty
assert LineString(None).is_empty
assert LineString([]).is_empty
assert LineString(empty_generator()).is_empty
def test_empty_multilinestring(self):
assert MultiLineString([]).is_empty
def test_empty_polygon(self):
assert Polygon().is_empty
assert Polygon(None).is_empty
assert Polygon([]).is_empty
assert Polygon(empty_generator()).is_empty
def test_empty_multipolygon(self):
assert MultiPolygon([]).is_empty
def test_empty_linear_ring(self):
assert LinearRing().is_empty
assert LinearRing(None).is_empty
assert LinearRing([]).is_empty
assert LinearRing(empty_generator()).is_empty
def test_numpy_object_array():
geoms = [Point(), GeometryCollection()]
arr = np.empty(2, object)
arr[:] = geoms
def test_shape_empty():
empty_mp = MultiPolygon()
empty_json = mapping(empty_mp)
empty_shape = shape(empty_json)
assert empty_shape.is_empty
@pytest.mark.parametrize(
"geom",
[
Point(),
LineString(),
Polygon(),
MultiPoint(),
MultiLineString(),
MultiPolygon(),
GeometryCollection(),
LinearRing(),
],
)
def test_empty_geometry_bounds(geom):
"""The bounds of an empty geometry is a tuple of NaNs"""
assert len(geom.bounds) == 4
assert all(math.isnan(v) for v in geom.bounds)
@@ -0,0 +1,237 @@
import numpy as np
import pytest
import shapely
from shapely import LinearRing, LineString, MultiLineString, Point, Polygon
from shapely.tests.common import all_types, all_types_z, ignore_invalid
@pytest.mark.parametrize("geom", all_types + all_types_z)
def test_equality(geom):
assert geom == geom
transformed = shapely.transform(geom, lambda x: x, include_z=True)
assert geom == transformed
assert not (geom != transformed)
@pytest.mark.parametrize(
"left, right",
[
# (slightly) different coordinate values
(LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 2)])),
(LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 1 + 1e-12)])),
# different coordinate order
(LineString([(0, 0), (1, 1)]), LineString([(1, 1), (0, 0)])),
# different number of coordinates (but spatially equal)
(LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 1), (1, 1)])),
(LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0.5, 0.5), (1, 1)])),
# different order of sub-geometries
(
MultiLineString([[(1, 1), (2, 2)], [(2, 2), (3, 3)]]),
MultiLineString([[(2, 2), (3, 3)], [(1, 1), (2, 2)]]),
),
],
)
def test_equality_false(left, right):
assert left != right
with ignore_invalid():
cases1 = [
(LineString([(0, 1), (2, np.nan)]), LineString([(0, 1), (2, np.nan)])),
(
LineString([(0, 1), (np.nan, np.nan)]),
LineString([(0, 1), (np.nan, np.nan)]),
),
(LineString([(np.nan, 1), (2, 3)]), LineString([(np.nan, 1), (2, 3)])),
(LineString([(0, np.nan), (2, 3)]), LineString([(0, np.nan), (2, 3)])),
(
LineString([(np.nan, np.nan), (np.nan, np.nan)]),
LineString([(np.nan, np.nan), (np.nan, np.nan)]),
),
# NaN as explicit Z coordinate
# TODO: if first z is NaN -> considered as 2D -> tested below explicitly
# (
# LineString([(0, 1, np.nan), (2, 3, np.nan)]),
# LineString([(0, 1, np.nan), (2, 3, np.nan)]),
# ),
(
LineString([(0, 1, 2), (2, 3, np.nan)]),
LineString([(0, 1, 2), (2, 3, np.nan)]),
),
# (
# LineString([(0, 1, np.nan), (2, 3, 4)]),
# LineString([(0, 1, np.nan), (2, 3, 4)]),
# ),
]
@pytest.mark.parametrize("left, right", cases1)
def test_equality_with_nan(left, right):
# TODO currently those evaluate as not equal, but we are considering to change this
# assert left == right
assert not (left == right)
# assert not (left != right)
assert left != right
with ignore_invalid():
cases2 = [
(
LineString([(0, 1, np.nan), (2, 3, np.nan)]),
LineString([(0, 1, np.nan), (2, 3, np.nan)]),
),
(
LineString([(0, 1, np.nan), (2, 3, 4)]),
LineString([(0, 1, np.nan), (2, 3, 4)]),
),
]
@pytest.mark.parametrize("left, right", cases2)
def test_equality_with_nan_z(left, right):
# TODO: those are currently considered equal because z dimension is ignored
if shapely.geos_version < (3, 12, 0):
assert left == right
assert not (left != right)
else:
# on GEOS main z dimension is not ignored -> NaNs cause inequality
assert left != right
with ignore_invalid():
cases3 = [
(LineString([(0, np.nan), (2, 3)]), LineString([(0, 1), (2, 3)])),
(LineString([(0, 1), (2, np.nan)]), LineString([(0, 1), (2, 3)])),
(LineString([(0, 1, np.nan), (2, 3, 4)]), LineString([(0, 1, 2), (2, 3, 4)])),
(LineString([(0, 1, 2), (2, 3, np.nan)]), LineString([(0, 1, 2), (2, 3, 4)])),
]
@pytest.mark.parametrize("left, right", cases3)
def test_equality_with_nan_false(left, right):
assert left != right
def test_equality_with_nan_z_false():
with ignore_invalid():
left = LineString([(0, 1, np.nan), (2, 3, np.nan)])
right = LineString([(0, 1, np.nan), (2, 3, 4)])
if shapely.geos_version < (3, 10, 0):
# GEOS <= 3.9 fill the NaN with 0, so the z dimension is different
# assert left != right
# however, has_z still returns False, so z dimension is ignored in .coords
assert left == right
elif shapely.geos_version < (3, 12, 0):
# GEOS 3.10-3.11 ignore NaN for Z also when explicitly created with 3D
# and so the geometries are considered as 2D (and thus z dimension is ignored)
assert left == right
else:
assert left != right
def test_equality_z():
# different dimensionality
geom1 = Point(0, 1)
geom2 = Point(0, 1, 0)
assert geom1 != geom2
# different dimensionality with NaN z
geom2 = Point(0, 1, np.nan)
if shapely.geos_version < (3, 10, 0):
# GEOS < 3.8 fill the NaN with 0, so the z dimension is different
# assert geom1 != geom2
# however, has_z still returns False, so z dimension is ignored in .coords
assert geom1 == geom2
elif shapely.geos_version < (3, 12, 0):
# GEOS 3.10-3.11 ignore NaN for Z also when explicitly created with 3D
# and so the geometries are considered as 2D (and thus z dimension is ignored)
assert geom1 == geom2
else:
assert geom1 != geom2
def test_equality_exact_type():
# geometries with different type but same coord seq are not equal
geom1 = LineString([(0, 0), (1, 1), (0, 1), (0, 0)])
geom2 = LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)])
geom3 = Polygon([(0, 0), (1, 1), (0, 1), (0, 0)])
assert geom1 != geom2
assert geom1 != geom3
assert geom2 != geom3
# empty with different type
geom1 = shapely.from_wkt("POINT EMPTY")
geom2 = shapely.from_wkt("LINESTRING EMPTY")
assert geom1 != geom2
def test_equality_polygon():
# different exterior rings
geom1 = shapely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
geom2 = shapely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 15, 0 0))")
assert geom1 != geom2
# different number of holes
geom1 = shapely.from_wkt(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1))"
)
geom2 = shapely.from_wkt(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), (3 3, 4 3, 4 4, 3 3))"
)
assert geom1 != geom2
# different order of holes
geom1 = shapely.from_wkt(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (3 3, 4 3, 4 4, 3 3), (1 1, 2 1, 2 2, 1 1))"
)
geom2 = shapely.from_wkt(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), (3 3, 4 3, 4 4, 3 3))"
)
assert geom1 != geom2
@pytest.mark.parametrize("geom", all_types)
def test_comparison_notimplemented(geom):
# comparing to a non-geometry class should return NotImplemented in __eq__
# to ensure proper delegation to other (eg to ensure comparison of scalar
# with array works)
# https://github.com/shapely/shapely/issues/1056
assert geom.__eq__(1) is NotImplemented
# with array
arr = np.array([geom, geom], dtype=object)
result = arr == geom
assert isinstance(result, np.ndarray)
assert result.all()
result = geom == arr
assert isinstance(result, np.ndarray)
assert result.all()
result = arr != geom
assert isinstance(result, np.ndarray)
assert not result.any()
result = geom != arr
assert isinstance(result, np.ndarray)
assert not result.any()
def test_comparison_not_supported():
geom1 = Point(1, 1)
geom2 = Point(2, 2)
with pytest.raises(TypeError, match="not supported between instances"):
geom1 > geom2
with pytest.raises(TypeError, match="not supported between instances"):
geom1 < geom2
with pytest.raises(TypeError, match="not supported between instances"):
geom1 >= geom2
with pytest.raises(TypeError, match="not supported between instances"):
geom1 <= geom2
@@ -0,0 +1,116 @@
import pytest
from shapely import Point, Polygon
from shapely.geos import geos_version
def test_format_invalid():
# check invalid spec formats
pt = Point(1, 2)
test_list = [
("5G", ValueError, "invalid format specifier"),
(".f", ValueError, "invalid format specifier"),
("0.2e", ValueError, "invalid format specifier"),
(".1x", ValueError, "hex representation does not specify precision"),
]
for format_spec, err, match in test_list:
with pytest.raises(err, match=match):
format(pt, format_spec)
def test_format_point():
# example coordinate data
xy1 = (0.12345678901234567, 1.2345678901234567e10)
xy2 = (-169.910918, -18.997564)
xyz3 = (630084, 4833438, 76)
# list of tuples to test; see structure at top of the for-loop
test_list = [
(".0f", xy1, "POINT (0 12345678901)", True),
(".1f", xy1, "POINT (0.1 12345678901.2)", True),
("0.2f", xy2, "POINT (-169.91 -19.00)", True),
(".3F", (float("inf"), -float("inf")), "POINT (INF -INF)", True),
]
if geos_version < (3, 10, 0):
# 'g' format varies depending on GEOS version
test_list += [
(".1g", xy1, "POINT (0.1 1e+10)", True),
(".6G", xy1, "POINT (0.123457 1.23457E+10)", True),
("0.12g", xy1, "POINT (0.123456789012 12345678901.2)", True),
("0.4g", xy2, "POINT (-169.9 -19)", True),
]
else:
test_list += [
(".1g", xy1, "POINT (0.1 12345678901.2)", False),
(".6G", xy1, "POINT (0.123457 12345678901.234568)", False),
("0.12g", xy1, "POINT (0.123456789012 12345678901.234568)", False),
("g", xy2, "POINT (-169.910918 -18.997564)", False),
("0.2g", xy2, "POINT (-169.91 -19)", False),
]
# 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),
("g", xyz3, "POINT Z (630084 4833438 76)", False),
]
for format_spec, coords, expt_wkt, same_python_float in test_list:
pt = Point(*coords)
# basic checks
assert f"{pt}" == pt.wkt
assert format(pt, "") == pt.wkt
assert format(pt, "x") == pt.wkb_hex.lower()
assert format(pt, "X") == pt.wkb_hex
# check formatted WKT to expected
assert format(pt, format_spec) == expt_wkt, format_spec
# check Python's format consistency
text_coords = expt_wkt[expt_wkt.index("(") + 1 : expt_wkt.index(")")]
is_same = []
for coord, expt_coord in zip(coords, text_coords.split()):
py_fmt_float = format(float(coord), format_spec)
if same_python_float:
assert py_fmt_float == expt_coord, format_spec
else:
is_same.append(py_fmt_float == expt_coord)
if not same_python_float:
assert not all(is_same), f"{format_spec!r} with {expt_wkt}"
def test_format_polygon():
# check basic cases
poly = Point(0, 0).buffer(10, 2)
assert f"{poly}" == poly.wkt
assert format(poly, "") == poly.wkt
assert format(poly, "x") == poly.wkb_hex.lower()
assert format(poly, "X") == poly.wkb_hex
# Use f-strings with extra characters and rounding precision
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):
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:
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))"
)
# check empty
empty = Polygon()
assert f"{empty}" == "POLYGON EMPTY"
assert format(empty, "") == empty.wkt
assert format(empty, ".2G") == empty.wkt
assert format(empty, "x") == empty.wkb_hex.lower()
assert format(empty, "X") == empty.wkb_hex
@@ -0,0 +1,281 @@
import platform
import weakref
import numpy as np
import pytest
import shapely
from shapely import (
GeometryCollection,
LinearRing,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
from shapely.errors import ShapelyDeprecationWarning
from shapely.testing import assert_geometries_equal
def test_polygon():
assert bool(Polygon()) is False
def test_linestring():
assert bool(LineString()) is False
def test_point():
assert bool(Point()) is False
def test_geometry_collection():
assert bool(GeometryCollection()) is False
geometries_all_types = [
Point(1, 1),
LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]),
LineString([(0, 0), (1, 1), (0, 1), (0, 0)]),
Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
MultiPoint([(1, 1)]),
MultiLineString([[(0, 0), (1, 1), (0, 1), (0, 0)]]),
MultiPolygon([Polygon([(0, 0), (1, 1), (0, 1), (0, 0)])]),
GeometryCollection([Point(1, 1)]),
]
@pytest.mark.skipif(
platform.python_implementation() == "PyPy",
reason="Setting custom attributes doesn't fail on PyPy",
)
@pytest.mark.parametrize("geom", geometries_all_types)
def test_setattr_disallowed(geom):
with pytest.raises(AttributeError):
geom.name = "test"
@pytest.mark.parametrize("geom", geometries_all_types)
def test_weakrefable(geom):
_ = weakref.ref(geom)
def test_base_class_not_callable():
with pytest.raises(TypeError):
shapely.Geometry("POINT (1 1)")
def test_GeometryType_deprecated():
geom = Point(1, 1)
with pytest.warns(ShapelyDeprecationWarning):
geom_type = geom.geometryType()
assert geom_type == geom.geom_type
def test_type_deprecated():
geom = Point(1, 1)
with pytest.warns(ShapelyDeprecationWarning):
geom_type = geom.type
assert geom_type == geom.geom_type
@pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10")
def test_segmentize():
line = LineString([(0, 0), (0, 10)])
result = line.segmentize(max_segment_length=5)
assert result.equals(LineString([(0, 0), (0, 5), (0, 10)]))
@pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7")
def test_reverse():
coords = [(0, 0), (1, 2)]
line = LineString(coords)
result = line.reverse()
assert result.coords[:] == coords[::-1]
@pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9")
@pytest.mark.parametrize(
"op", ["union", "intersection", "difference", "symmetric_difference"]
)
@pytest.mark.parametrize("grid_size", [0, 1, 2])
def test_binary_op_grid_size(op, grid_size):
geom1 = shapely.box(0, 0, 2.5, 2.5)
geom2 = shapely.box(2, 2, 3, 3)
result = getattr(geom1, op)(geom2, grid_size=grid_size)
expected = getattr(shapely, op)(geom1, geom2, grid_size=grid_size)
assert result == expected
@pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10")
def test_dwithin():
point = Point(1, 1)
line = LineString([(0, 0), (0, 10)])
assert point.dwithin(line, 0.5) is False
assert point.dwithin(line, 1.5) is True
def test_contains_properly():
polygon = Polygon([(0, 0), (10, 10), (10, -10)])
line = LineString([(0, 0), (10, 0)])
assert polygon.contains_properly(line) is False
assert polygon.contains(line) is True
@pytest.mark.parametrize(
"op", ["convex_hull", "envelope", "oriented_envelope", "minimum_rotated_rectangle"]
)
def test_constructive_properties(op):
geom = LineString([(0, 0), (0, 10), (10, 10)])
result = getattr(geom, op)
expected = getattr(shapely, op)(geom)
assert result == expected
@pytest.mark.parametrize(
"op",
[
"crosses",
"contains",
"contains_properly",
"covered_by",
"covers",
"disjoint",
"equals",
"intersects",
"overlaps",
"touches",
"within",
],
)
def test_array_argument_binary_predicates(op):
polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)])
result = getattr(polygon, op)(points)
assert isinstance(result, np.ndarray)
expected = np.array([getattr(polygon, op)(p) for p in points], dtype=bool)
np.testing.assert_array_equal(result, expected)
# check scalar
result = getattr(polygon, op)(points[0])
assert type(result) is bool
@pytest.mark.parametrize(
"op, kwargs",
[
pytest.param(
"dwithin",
dict(distance=0.5),
marks=pytest.mark.skipif(
shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10"
),
),
("equals_exact", dict(tolerance=0.01)),
("relate_pattern", dict(pattern="T*F**F***")),
],
)
def test_array_argument_binary_predicates2(op, kwargs):
polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)])
result = getattr(polygon, op)(points, **kwargs)
assert isinstance(result, np.ndarray)
expected = np.array([getattr(polygon, op)(p, **kwargs) for p in points], dtype=bool)
np.testing.assert_array_equal(result, expected)
# check scalar
result = getattr(polygon, op)(points[0], **kwargs)
assert type(result) is bool
@pytest.mark.parametrize(
"op",
[
"difference",
"intersection",
"symmetric_difference",
"union",
],
)
def test_array_argument_binary_geo(op):
box = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
polygons = shapely.buffer(shapely.points([(0, 0), (0.5, 0.5), (1, 1)]), 0.5)
result = getattr(box, op)(polygons)
assert isinstance(result, np.ndarray)
expected = np.array([getattr(box, op)(g) for g in polygons], dtype=object)
assert_geometries_equal(result, expected)
# check scalar
result = getattr(box, op)(polygons[0])
assert isinstance(result, (Polygon, MultiPolygon))
@pytest.mark.parametrize("op", ["distance", "hausdorff_distance"])
def test_array_argument_float(op):
polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)])
result = getattr(polygon, op)(points)
assert isinstance(result, np.ndarray)
expected = np.array([getattr(polygon, op)(p) for p in points], dtype="float64")
np.testing.assert_array_equal(result, expected)
# check scalar
result = getattr(polygon, op)(points[0])
assert type(result) is float
@pytest.mark.parametrize("op", ["line_interpolate_point", "interpolate"])
def test_array_argument_linear_point(op):
line = LineString([(0, 0), (0, 1), (1, 1)])
distances = np.array([0, 0.5, 1])
result = getattr(line, op)(distances)
assert isinstance(result, np.ndarray)
expected = np.array(
[line.line_interpolate_point(d) for d in distances], dtype=object
)
assert_geometries_equal(result, expected)
# check scalar
result = getattr(line, op)(distances[0])
assert isinstance(result, Point)
@pytest.mark.parametrize("op", ["line_locate_point", "project"])
def test_array_argument_linear_float(op):
line = LineString([(0, 0), (0, 1), (1, 1)])
points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)])
result = getattr(line, op)(points)
assert isinstance(result, np.ndarray)
expected = np.array([line.line_locate_point(p) for p in points], dtype="float64")
np.testing.assert_array_equal(result, expected)
# check scalar
result = getattr(line, op)(points[0])
assert type(result) is float
def test_array_argument_buffer():
point = Point(1, 1)
distances = np.array([0, 0.5, 1])
result = point.buffer(distances)
assert isinstance(result, np.ndarray)
expected = np.array([point.buffer(d) for d in distances], dtype=object)
assert_geometries_equal(result, expected)
# check scalar
result = point.buffer(distances[0])
assert isinstance(result, Polygon)
@@ -0,0 +1,28 @@
import pytest
import shapely
from shapely.affinity import translate
from shapely.geometry import GeometryCollection, LineString, MultiPoint, Point
@pytest.mark.parametrize(
"geom",
[
Point(1, 2),
MultiPoint([(1, 2), (3, 4)]),
LineString([(1, 2), (3, 4)]),
Point(0, 0).buffer(1.0),
GeometryCollection([Point(1, 2), LineString([(1, 2), (3, 4)])]),
],
ids=[
"Point",
"MultiPoint",
"LineString",
"Polygon",
"GeometryCollection",
],
)
def test_hash(geom):
h1 = hash(geom)
assert h1 == hash(shapely.from_wkb(geom.wkb))
assert h1 != hash(translate(geom, 1.0, 2.0))
@@ -0,0 +1,213 @@
import numpy as np
import pytest
import shapely
from shapely import LinearRing, LineString, Point
from shapely.coords import CoordinateSequence
def test_from_coordinate_sequence():
# From coordinate tuples
line = LineString([(1.0, 2.0), (3.0, 4.0)])
assert len(line.coords) == 2
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
line = LineString([(1.0, 2.0), (3.0, 4.0)])
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
def test_from_coordinate_sequence_3D():
line = LineString([(1.0, 2.0, 3.0), (3.0, 4.0, 5.0)])
assert line.has_z
assert line.coords[:] == [(1.0, 2.0, 3.0), (3.0, 4.0, 5.0)]
def test_from_points():
# From Points
line = LineString([Point(1.0, 2.0), Point(3.0, 4.0)])
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
line = LineString([Point(1.0, 2.0), Point(3.0, 4.0)])
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
def test_from_mix():
# From mix of tuples and Points
line = LineString([Point(1.0, 2.0), (2.0, 3.0), Point(3.0, 4.0)])
assert line.coords[:] == [(1.0, 2.0), (2.0, 3.0), (3.0, 4.0)]
def test_from_linestring():
# From another linestring
line = LineString([(1.0, 2.0), (3.0, 4.0)])
copy = LineString(line)
assert copy.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
assert copy.geom_type == "LineString"
def test_from_linearring():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing(coords)
copy = LineString(ring)
assert copy.coords[:] == coords
assert copy.geom_type == "LineString"
def test_from_linestring_z():
coords = [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]
line = LineString(coords)
copy = LineString(line)
assert copy.coords[:] == coords
assert copy.geom_type == "LineString"
def test_from_generator():
gen = (coord for coord in [(1.0, 2.0), (3.0, 4.0)])
line = LineString(gen)
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
def test_from_empty():
line = LineString()
assert line.is_empty
assert isinstance(line.coords, CoordinateSequence)
assert line.coords[:] == []
line = LineString([])
assert line.is_empty
assert isinstance(line.coords, CoordinateSequence)
assert line.coords[:] == []
def test_from_numpy():
# Construct from a numpy array
line = LineString(np.array([[1.0, 2.0], [3.0, 4.0]]))
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
def test_numpy_empty_linestring_coords():
# Check empty
line = LineString([])
la = np.asarray(line.coords)
assert la.shape == (0, 2)
def test_numpy_object_array():
geom = LineString([(0.0, 0.0), (0.0, 1.0)])
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
@pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:")
def test_from_invalid_dim():
# TODO(shapely-2.0) better error message?
# pytest.raises(ValueError, match="at least 2 coordinate tuples|at least 2 coordinates"):
with pytest.raises(shapely.GEOSException):
LineString([(1, 2)])
# exact error depends on numpy version
with pytest.raises((ValueError, TypeError)):
LineString([(1, 2, 3), (4, 5)])
with pytest.raises((ValueError, TypeError)):
LineString([(1, 2), (3, 4, 5)])
msg = r"The ordinate \(last\) dimension should be 2 or 3, got {}"
with pytest.raises(ValueError, match=msg.format(4)):
LineString([(1, 2, 3, 4), (4, 5, 6, 7)])
with pytest.raises(ValueError, match=msg.format(1)):
LineString([(1,), (4,)])
def test_from_single_coordinate():
"""Test for issue #486"""
coords = [[-122.185933073564, 37.3629353839073]]
with pytest.raises(shapely.GEOSException):
ls = LineString(coords)
ls.geom_type # caused segfault before fix
class TestLineString:
def test_linestring(self):
# From coordinate tuples
line = LineString([(1.0, 2.0), (3.0, 4.0)])
assert len(line.coords) == 2
assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
# Bounds
assert line.bounds == (1.0, 2.0, 3.0, 4.0)
# Coordinate access
assert tuple(line.coords) == ((1.0, 2.0), (3.0, 4.0))
assert line.coords[0] == (1.0, 2.0)
assert line.coords[1] == (3.0, 4.0)
with pytest.raises(IndexError):
line.coords[2] # index out of range
# Geo interface
assert line.__geo_interface__ == {
"type": "LineString",
"coordinates": ((1.0, 2.0), (3.0, 4.0)),
}
def test_linestring_empty(self):
# Test Non-operability of Null geometry
l_null = LineString()
assert l_null.wkt == "LINESTRING EMPTY"
assert l_null.length == 0.0
def test_equals_argument_order(self):
"""
Test equals predicate functions correctly regardless of the order
of the inputs. See issue #317.
"""
coords = ((0, 0), (1, 0), (1, 1), (0, 0))
ls = LineString(coords)
lr = LinearRing(coords)
assert ls.__eq__(lr) is False # previously incorrectly returned True
assert lr.__eq__(ls) is False
assert (ls == lr) is False
assert (lr == ls) is False
ls_clone = LineString(coords)
lr_clone = LinearRing(coords)
assert ls.__eq__(ls_clone) is True
assert lr.__eq__(lr_clone) is True
assert (ls == ls_clone) is True
assert (lr == lr_clone) is True
def test_numpy_linestring_coords(self):
from numpy.testing import assert_array_equal
line = LineString([(1.0, 2.0), (3.0, 4.0)])
expected = np.array([[1.0, 2.0], [3.0, 4.0]])
# Coordinate sequences can be adapted as well
la = np.asarray(line.coords)
assert_array_equal(la, expected)
def test_linestring_immutable():
line = LineString([(1.0, 2.0), (3.0, 4.0)])
with pytest.raises(AttributeError):
line.coords = [(-1.0, -1.0), (1.0, 1.0)]
with pytest.raises(TypeError):
line.coords[0] = (-1.0, -1.0)
def test_linestring_array_coercion():
# don't convert to array of coordinates, keep objects
line = LineString([(1.0, 2.0), (3.0, 4.0)])
arr = np.array(line)
assert arr.ndim == 0
assert arr.size == 1
assert arr.dtype == np.dtype("object")
assert arr.item() == line
@@ -0,0 +1,11 @@
import numpy as np
test_int_types = [int, np.int16, np.int32, np.int64]
class MultiGeometryTestCase:
def subgeom_access_test(self, cls, geoms):
geom = cls(geoms)
for t in test_int_types:
for i, g in enumerate(geoms):
assert geom.geoms[t(i)] == geoms[i]
@@ -0,0 +1,78 @@
import numpy as np
import pytest
from shapely import LineString, MultiLineString
from shapely.errors import EmptyPartError
from shapely.geometry.base import dump_coords
from shapely.tests.geometry.test_multi import MultiGeometryTestCase
class TestMultiLineString(MultiGeometryTestCase):
def test_multilinestring(self):
# From coordinate tuples
geom = MultiLineString([[(1.0, 2.0), (3.0, 4.0)]])
assert isinstance(geom, MultiLineString)
assert len(geom.geoms) == 1
assert dump_coords(geom) == [[(1.0, 2.0), (3.0, 4.0)]]
# From lines
a = LineString([(1.0, 2.0), (3.0, 4.0)])
ml = MultiLineString([a])
assert len(ml.geoms) == 1
assert dump_coords(ml) == [[(1.0, 2.0), (3.0, 4.0)]]
# From another multi-line
ml2 = MultiLineString(ml)
assert len(ml2.geoms) == 1
assert dump_coords(ml2) == [[(1.0, 2.0), (3.0, 4.0)]]
# Sub-geometry Access
geom = MultiLineString([(((0.0, 0.0), (1.0, 2.0)))])
assert isinstance(geom.geoms[0], LineString)
assert dump_coords(geom.geoms[0]) == [(0.0, 0.0), (1.0, 2.0)]
with pytest.raises(IndexError): # index out of range
geom.geoms[1]
# Geo interface
assert geom.__geo_interface__ == {
"type": "MultiLineString",
"coordinates": (((0.0, 0.0), (1.0, 2.0)),),
}
def test_from_multilinestring_z(self):
coords1 = [(0.0, 1.0, 2.0), (3.0, 4.0, 5.0)]
coords2 = [(6.0, 7.0, 8.0), (9.0, 10.0, 11.0)]
# From coordinate tuples
ml = MultiLineString([coords1, coords2])
copy = MultiLineString(ml)
assert isinstance(copy, MultiLineString)
assert copy.geom_type == "MultiLineString"
assert len(copy.geoms) == 2
assert dump_coords(copy.geoms[0]) == coords1
assert dump_coords(copy.geoms[1]) == coords2
def test_numpy(self):
# Construct from a numpy array
geom = MultiLineString([np.array(((0.0, 0.0), (1.0, 2.0)))])
assert isinstance(geom, MultiLineString)
assert len(geom.geoms) == 1
assert dump_coords(geom) == [[(0.0, 0.0), (1.0, 2.0)]]
def test_subgeom_access(self):
line0 = LineString([(0.0, 1.0), (2.0, 3.0)])
line1 = LineString([(4.0, 5.0), (6.0, 7.0)])
self.subgeom_access_test(MultiLineString, [line0, line1])
def test_create_multi_with_empty_component(self):
msg = "Can't create MultiLineString with empty component"
with pytest.raises(EmptyPartError, match=msg):
MultiLineString([LineString([(0, 0), (1, 1), (2, 2)]), LineString()]).wkt
def test_numpy_object_array():
geom = MultiLineString([[[5.0, 6.0], [7.0, 8.0]]])
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
@@ -0,0 +1,78 @@
import numpy as np
import pytest
from shapely import MultiPoint, Point
from shapely.errors import EmptyPartError
from shapely.geometry.base import dump_coords
from shapely.tests.geometry.test_multi import MultiGeometryTestCase
class TestMultiPoint(MultiGeometryTestCase):
def test_multipoint(self):
# From coordinate tuples
geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)])
assert len(geom.geoms) == 2
assert dump_coords(geom) == [[(1.0, 2.0)], [(3.0, 4.0)]]
# From points
geom = MultiPoint([Point(1.0, 2.0), Point(3.0, 4.0)])
assert len(geom.geoms) == 2
assert dump_coords(geom) == [[(1.0, 2.0)], [(3.0, 4.0)]]
# From another multi-point
geom2 = MultiPoint(geom)
assert len(geom2.geoms) == 2
assert dump_coords(geom2) == [[(1.0, 2.0)], [(3.0, 4.0)]]
# Sub-geometry Access
assert isinstance(geom.geoms[0], Point)
assert geom.geoms[0].x == 1.0
assert geom.geoms[0].y == 2.0
with pytest.raises(IndexError): # index out of range
geom.geoms[2]
# Geo interface
assert geom.__geo_interface__ == {
"type": "MultiPoint",
"coordinates": ((1.0, 2.0), (3.0, 4.0)),
}
def test_multipoint_from_numpy(self):
# Construct from a numpy array
geom = MultiPoint(np.array([[0.0, 0.0], [1.0, 2.0]]))
assert isinstance(geom, MultiPoint)
assert len(geom.geoms) == 2
assert dump_coords(geom) == [[(0.0, 0.0)], [(1.0, 2.0)]]
def test_subgeom_access(self):
p0 = Point(1.0, 2.0)
p1 = Point(3.0, 4.0)
self.subgeom_access_test(MultiPoint, [p0, p1])
def test_create_multi_with_empty_component(self):
msg = "Can't create MultiPoint with empty component"
with pytest.raises(EmptyPartError, match=msg):
MultiPoint([Point(0, 0), Point()]).wkt
def test_multipoint_array_coercion():
geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)])
arr = np.array(geom)
assert arr.ndim == 0
assert arr.size == 1
assert arr.dtype == np.dtype("object")
assert arr.item() == geom
def test_numpy_object_array():
geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)])
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
def test_len_raises():
geom = MultiPoint([[5.0, 6.0], [7.0, 8.0]])
with pytest.raises(TypeError):
len(geom)
@@ -0,0 +1,134 @@
import numpy as np
import pytest
from shapely import MultiPolygon, Polygon
from shapely.geometry.base import dump_coords
from shapely.tests.geometry.test_multi import MultiGeometryTestCase
class TestMultiPolygon(MultiGeometryTestCase):
def test_multipolygon(self):
# From coordinate tuples
coords = [
(
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
[((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))],
)
]
geom = MultiPolygon(coords)
assert isinstance(geom, MultiPolygon)
assert len(geom.geoms) == 1
assert dump_coords(geom) == [
[
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)],
]
]
# Or without holes
coords2 = [(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),)]
geom = MultiPolygon(coords2)
assert isinstance(geom, MultiPolygon)
assert len(geom.geoms) == 1
assert dump_coords(geom) == [
[
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
]
]
# Or from polygons
p = Polygon(
((0, 0), (0, 1), (1, 1), (1, 0)),
[((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))],
)
geom = MultiPolygon([p])
assert len(geom.geoms) == 1
assert dump_coords(geom) == [
[
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)],
]
]
# Or from another multi-polygon
geom2 = MultiPolygon(geom)
assert len(geom2.geoms) == 1
assert dump_coords(geom2) == [
[
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)],
]
]
# Sub-geometry Access
assert isinstance(geom.geoms[0], Polygon)
assert dump_coords(geom.geoms[0]) == [
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)],
]
with pytest.raises(IndexError): # index out of range
geom.geoms[1]
# Geo interface
assert geom.__geo_interface__ == {
"type": "MultiPolygon",
"coordinates": [
(
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),
((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)),
)
],
}
def test_subgeom_access(self):
poly0 = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
poly1 = Polygon([(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)])
self.subgeom_access_test(MultiPolygon, [poly0, poly1])
def test_fail_list_of_multipolygons():
"""A list of multipolygons is not a valid multipolygon ctor argument"""
multi = MultiPolygon(
[
(
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
[((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))],
)
]
)
with pytest.raises(ValueError):
MultiPolygon([multi])
def test_numpy_object_array():
geom = MultiPolygon(
[
(
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
[((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))],
)
]
)
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
@@ -0,0 +1,188 @@
import numpy as np
import pytest
from shapely import Point
from shapely.coords import CoordinateSequence
from shapely.errors import DimensionError
def test_from_coordinates():
# 2D points
p = Point(1.0, 2.0)
assert p.coords[:] == [(1.0, 2.0)]
assert p.has_z is False
# 3D Point
p = Point(1.0, 2.0, 3.0)
assert p.coords[:] == [(1.0, 2.0, 3.0)]
assert p.has_z
# empty
p = Point()
assert p.is_empty
assert isinstance(p.coords, CoordinateSequence)
assert p.coords[:] == []
def test_from_sequence():
# From single coordinate pair
p = Point((3.0, 4.0))
assert p.coords[:] == [(3.0, 4.0)]
p = Point([3.0, 4.0])
assert p.coords[:] == [(3.0, 4.0)]
# From coordinate sequence
p = Point([(3.0, 4.0)])
assert p.coords[:] == [(3.0, 4.0)]
p = Point([[3.0, 4.0]])
assert p.coords[:] == [(3.0, 4.0)]
# 3D
p = Point((3.0, 4.0, 5.0))
assert p.coords[:] == [(3.0, 4.0, 5.0)]
p = Point([3.0, 4.0, 5.0])
assert p.coords[:] == [(3.0, 4.0, 5.0)]
p = Point([(3.0, 4.0, 5.0)])
assert p.coords[:] == [(3.0, 4.0, 5.0)]
def test_from_numpy():
# Construct from a numpy array
p = Point(np.array([1.0, 2.0]))
assert p.coords[:] == [(1.0, 2.0)]
p = Point(np.array([1.0, 2.0, 3.0]))
assert p.coords[:] == [(1.0, 2.0, 3.0)]
def test_from_numpy_xy():
# Construct from separate x, y numpy arrays - if those are length 1,
# this is allowed for compat with shapely 1.8
# (https://github.com/shapely/shapely/issues/1587)
p = Point(np.array([1.0]), np.array([2.0]))
assert p.coords[:] == [(1.0, 2.0)]
p = Point(np.array([1.0]), np.array([2.0]), np.array([3.0]))
assert p.coords[:] == [(1.0, 2.0, 3.0)]
def test_from_point():
# From another point
p = Point(3.0, 4.0)
q = Point(p)
assert q.coords[:] == [(3.0, 4.0)]
p = Point(3.0, 4.0, 5.0)
q = Point(p)
assert q.coords[:] == [(3.0, 4.0, 5.0)]
def test_from_generator():
gen = (coord for coord in [(1.0, 2.0)])
p = Point(gen)
assert p.coords[:] == [(1.0, 2.0)]
def test_from_invalid():
with pytest.raises(TypeError, match="takes at most 3 arguments"):
Point(1, 2, 3, 4)
# this worked in shapely 1.x, just ignoring the other coords
with pytest.raises(
ValueError, match="takes only scalar or 1-size vector arguments"
):
Point([(2, 3), (11, 4)])
class TestPoint:
def test_point(self):
# 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
with pytest.raises(DimensionError):
p.z
# Check 3D
p = Point(1.0, 2.0, 3.0)
assert p.coords[:] == [(1.0, 2.0, 3.0)]
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))
assert p.x == 3.0
assert p.y == 4.0
assert tuple(p.coords) == ((3.0, 4.0),)
assert p.coords[0] == (3.0, 4.0)
with pytest.raises(IndexError): # index out of range
p.coords[1]
# Bounds
assert p.bounds == (3.0, 4.0, 3.0, 4.0)
# Geo interface
assert p.__geo_interface__ == {"type": "Point", "coordinates": (3.0, 4.0)}
def test_point_empty(self):
# Test Non-operability of Null geometry
p_null = Point()
assert p_null.wkt == "POINT EMPTY"
assert p_null.coords[:] == []
assert p_null.area == 0.0
def test_coords(self):
# From Array.txt
p = Point(0.0, 0.0, 1.0)
coords = p.coords[0]
assert coords == (0.0, 0.0, 1.0)
# Convert to Numpy array, passing through Python sequence
a = np.asarray(coords)
assert a.ndim == 1
assert a.size == 3
assert a.shape == (3,)
def test_point_immutable():
p = Point(3.0, 4.0)
with pytest.raises(AttributeError):
p.coords = (2.0, 1.0)
with pytest.raises(TypeError):
p.coords[0] = (2.0, 1.0)
def test_point_array_coercion():
# don't convert to array of coordinates, keep objects
p = Point(3.0, 4.0)
arr = np.array(p)
assert arr.ndim == 0
assert arr.size == 1
assert arr.dtype == np.dtype("object")
assert arr.item() == p
def test_numpy_empty_point_coords():
pe = Point()
# Access the coords
a = np.asarray(pe.coords)
assert a.shape == (0, 2)
def test_numpy_object_array():
geom = Point(3.0, 4.0)
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
@@ -0,0 +1,463 @@
"""Polygons and Linear Rings
"""
import numpy as np
import pytest
from shapely import LinearRing, LineString, Point, Polygon
from shapely.coords import CoordinateSequence
from shapely.errors import TopologicalError
from shapely.wkb import loads as load_wkb
def test_empty_linearring_coords():
assert LinearRing().coords[:] == []
def test_linearring_from_coordinate_sequence():
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
assert ring.coords[:] == expected_coords
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
assert ring.coords[:] == expected_coords
def test_linearring_from_points():
# From Points
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)])
assert ring.coords[:] == expected_coords
def test_linearring_from_closed_linestring():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
line = LineString(coords)
ring = LinearRing(line)
assert len(ring.coords) == 4
assert ring.coords[:] == coords
assert ring.geom_type == "LinearRing"
def test_linearring_from_unclosed_linestring():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
line = LineString(coords[:-1]) # Pass in unclosed line
ring = LinearRing(line)
assert len(ring.coords) == 4
assert ring.coords[:] == coords
assert ring.geom_type == "LinearRing"
def test_linearring_from_invalid():
coords = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
line = LineString(coords)
assert not line.is_valid
with pytest.raises(TopologicalError):
LinearRing(line)
def test_linearring_from_too_short_linestring():
# Creation of LinearRing request at least 3 coordinates (unclosed) or
# 4 coordinates (closed)
coords = [(0.0, 0.0), (1.0, 1.0)]
line = LineString(coords)
with pytest.raises(ValueError, match="requires at least 4 coordinates"):
LinearRing(line)
def test_linearring_from_linearring():
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing(coords)
assert ring.coords[:] == coords
def test_linearring_from_generator():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
gen = (coord for coord in coords)
ring = LinearRing(gen)
assert ring.coords[:] == coords
def test_linearring_from_empty():
ring = LinearRing()
assert ring.is_empty
assert isinstance(ring.coords, CoordinateSequence)
assert ring.coords[:] == []
ring = LinearRing([])
assert ring.is_empty
assert isinstance(ring.coords, CoordinateSequence)
assert ring.coords[:] == []
def test_linearring_from_numpy():
# Construct from a numpy array
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing(np.array(coords))
assert ring.coords[:] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
def test_numpy_linearring_coords():
from numpy.testing import assert_array_equal
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
ra = np.asarray(ring.coords)
expected = np.asarray([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)])
assert_array_equal(ra, expected)
def test_numpy_empty_linearring_coords():
ring = LinearRing()
assert np.asarray(ring.coords).shape == (0, 2)
def test_numpy_object_array():
geom = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
ar = np.empty(1, object)
ar[:] = [geom]
assert ar[0] == geom
def test_polygon_from_coordinate_sequence():
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
# Construct a polygon, exterior ring only
polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
assert polygon.exterior.coords[:] == coords
assert len(polygon.interiors) == 0
polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
assert polygon.exterior.coords[:] == coords
assert len(polygon.interiors) == 0
def test_polygon_from_coordinate_sequence_with_holes():
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
# Interior rings (holes)
polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]])
assert polygon.exterior.coords[:] == coords
assert len(polygon.interiors) == 1
assert len(polygon.interiors[0].coords) == 5
# Multiple interior rings with different length
coords = [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]
holes = [
[(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)],
[(3, 3), (3, 4), (4, 5), (5, 4), (5, 3), (3, 3)],
]
polygon = Polygon(coords, holes)
assert polygon.exterior.coords[:] == coords
assert len(polygon.interiors) == 2
assert len(polygon.interiors[0].coords) == 5
assert len(polygon.interiors[1].coords) == 6
def test_polygon_from_linearring():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
ring = LinearRing(coords)
polygon = Polygon(ring)
assert polygon.exterior.coords[:] == coords
assert len(polygon.interiors) == 0
# from shell and holes linearrings
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
holes = [
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
]
polygon = Polygon(shell, holes)
assert polygon.exterior.coords[:] == shell.coords[:]
assert len(polygon.interiors) == 3
for i in range(3):
assert polygon.interiors[i].coords[:] == holes[i].coords[:]
def test_polygon_from_linestring():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
line = LineString(coords)
polygon = Polygon(line)
assert polygon.exterior.coords[:] == coords
# from unclosed linestring
line = LineString(coords[:-1])
polygon = Polygon(line)
assert polygon.exterior.coords[:] == coords
def test_polygon_from_points():
polygon = Polygon([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)])
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
assert polygon.exterior.coords[:] == expected_coords
def test_polygon_from_polygon():
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]
polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]])
# Test from another Polygon
copy = Polygon(polygon)
assert len(copy.exterior.coords) == 5
assert len(copy.interiors) == 1
assert len(copy.interiors[0].coords) == 5
def test_polygon_from_invalid():
# Error handling
with pytest.raises(ValueError):
# A LinearRing must have at least 3 coordinate tuples
Polygon([[1, 2], [2, 3]])
def test_polygon_from_empty():
polygon = Polygon()
assert polygon.is_empty
assert polygon.exterior.coords[:] == []
polygon = Polygon([])
assert polygon.is_empty
assert polygon.exterior.coords[:] == []
def test_polygon_from_numpy():
a = np.array(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)))
polygon = Polygon(a)
assert len(polygon.exterior.coords) == 5
assert polygon.exterior.coords[:] == [
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0),
(0.0, 0.0),
]
assert len(polygon.interiors) == 0
def test_polygon_from_generator():
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
gen = (coord for coord in coords)
polygon = Polygon(gen)
assert polygon.exterior.coords[:] == coords
class TestPolygon:
def test_linearring(self):
# Initialization
# Linear rings won't usually be created by users, but by polygons
coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
ring = LinearRing(coords)
assert len(ring.coords) == 5
assert ring.coords[0] == ring.coords[4]
assert ring.coords[0] == ring.coords[-1]
assert ring.is_ring is True
def test_polygon(self):
coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
# Construct a polygon, exterior ring only
polygon = Polygon(coords)
assert len(polygon.exterior.coords) == 5
# Ring Access
assert isinstance(polygon.exterior, LinearRing)
ring = polygon.exterior
assert len(ring.coords) == 5
assert ring.coords[0] == ring.coords[4]
assert ring.coords[0] == (0.0, 0.0)
assert ring.is_ring is True
assert len(polygon.interiors) == 0
# Create a new polygon from WKB
data = polygon.wkb
polygon = None
ring = None
polygon = load_wkb(data)
ring = polygon.exterior
assert len(ring.coords) == 5
assert ring.coords[0] == ring.coords[4]
assert ring.coords[0] == (0.0, 0.0)
assert ring.is_ring is True
polygon = None
# Interior rings (holes)
polygon = Polygon(
coords, [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))]
)
assert len(polygon.exterior.coords) == 5
assert len(polygon.interiors[0].coords) == 5
with pytest.raises(IndexError): # index out of range
polygon.interiors[1]
# Coordinate getter raises exceptions
with pytest.raises(NotImplementedError):
polygon.coords
# Geo interface
assert polygon.__geo_interface__ == {
"type": "Polygon",
"coordinates": (
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),
((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)),
),
}
def test_linearring_empty(self):
# Test Non-operability of Null rings
r_null = LinearRing()
assert r_null.wkt == "LINEARRING EMPTY"
assert r_null.length == 0.0
def test_dimensions(self):
# Background: see http://trac.gispython.org/lab/ticket/168
# http://lists.gispython.org/pipermail/community/2008-August/001859.html
coords = ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0))
polygon = Polygon(coords)
assert polygon._ndim == 3
gi = polygon.__geo_interface__
assert gi["coordinates"] == (
(
(0.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(1.0, 1.0, 0.0),
(1.0, 0.0, 0.0),
(0.0, 0.0, 0.0),
),
)
e = polygon.exterior
assert e._ndim == 3
gi = e.__geo_interface__
assert gi["coordinates"] == (
(0.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(1.0, 1.0, 0.0),
(1.0, 0.0, 0.0),
(0.0, 0.0, 0.0),
)
def test_attribute_chains(self):
# Attribute Chaining
# See also ticket #151.
p = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
assert list(p.boundary.coords) == [
(0.0, 0.0),
(0.0, 1.0),
(-1.0, 1.0),
(-1.0, 0.0),
(0.0, 0.0),
]
ec = list(Point(0.0, 0.0).buffer(1.0, 1).exterior.coords)
assert isinstance(ec, list) # TODO: this is a poor test
# Test chained access to interiors
p = Polygon(
[(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)],
[[(-0.25, 0.25), (-0.25, 0.75), (-0.75, 0.75), (-0.75, 0.25)]],
)
assert p.area == 0.75
"""Not so much testing the exact values here, which are the
responsibility of the geometry engine (GEOS), but that we can get
chain functions and properties using anonymous references.
"""
assert list(p.interiors[0].coords) == [
(-0.25, 0.25),
(-0.25, 0.75),
(-0.75, 0.75),
(-0.75, 0.25),
(-0.25, 0.25),
]
xy = list(p.interiors[0].buffer(1).exterior.coords)[0]
assert len(xy) == 2
# Test multiple operators, boundary of a buffer
ec = list(p.buffer(1).boundary.coords)
assert isinstance(ec, list) # TODO: this is a poor test
def test_empty_equality(self):
# Test equals operator, including empty geometries
# see issue #338
point1 = Point(0, 0)
polygon1 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
polygon2 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
polygon_empty1 = Polygon()
polygon_empty2 = Polygon()
assert point1 != polygon1
assert polygon_empty1 == polygon_empty2
assert polygon1 != polygon_empty1
assert polygon1 == polygon2
assert polygon_empty1 is not None
def test_from_bounds(self):
xmin, ymin, xmax, ymax = -180, -90, 180, 90
coords = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
assert Polygon(coords) == Polygon.from_bounds(xmin, ymin, xmax, ymax)
def test_empty_polygon_exterior(self):
p = Polygon()
assert p.exterior == LinearRing()
def test_linearring_immutable():
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
with pytest.raises(AttributeError):
ring.coords = [(1.0, 1.0), (2.0, 2.0), (1.0, 2.0)]
with pytest.raises(TypeError):
ring.coords[0] = (1.0, 1.0)
class TestLinearRingGetItem:
def test_index_linearring(self):
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
holes = [
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
]
g = Polygon(shell, holes)
for i in range(-3, 3):
assert g.interiors[i].equals(holes[i])
with pytest.raises(IndexError):
g.interiors[3]
with pytest.raises(IndexError):
g.interiors[-4]
def test_index_linearring_misc(self):
g = Polygon() # empty
with pytest.raises(IndexError):
g.interiors[0]
with pytest.raises(TypeError):
g.interiors[0.0]
def test_slice_linearring(self):
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
holes = [
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
]
g = Polygon(shell, holes)
t = [a.equals(b) for (a, b) in zip(g.interiors[1:], holes[1:])]
assert all(t)
t = [a.equals(b) for (a, b) in zip(g.interiors[:-1], holes[:-1])]
assert all(t)
t = [a.equals(b) for (a, b) in zip(g.interiors[::-1], holes[::-1])]
assert all(t)
t = [a.equals(b) for (a, b) in zip(g.interiors[::2], holes[::2])]
assert all(t)
t = [a.equals(b) for (a, b) in zip(g.interiors[:3], holes[:3])]
assert all(t)
assert g.interiors[3:] == holes[3:] == []
@@ -0,0 +1,10 @@
import sys
import numpy
from shapely.geos import geos_version_string
# Show some diagnostic information; handy for CI
print("Python version: " + sys.version.replace("\n", " "))
print("GEOS version: " + geos_version_string)
print("Numpy version: " + numpy.version.version)
@@ -0,0 +1,21 @@
import numpy
import pytest
from shapely.geos import geos_version
requires_geos_38 = pytest.mark.skipif(
geos_version < (3, 8, 0), reason="GEOS >= 3.8.0 is required."
)
requires_geos_342 = pytest.mark.skipif(
geos_version < (3, 4, 2), reason="GEOS > 3.4.2 is required."
)
shapely20_todo = pytest.mark.xfail(
strict=True, reason="Not yet implemented for Shapely 2.0"
)
shapely20_wontfix = pytest.mark.xfail(strict=True, reason="Will fail for Shapely 2.0")
def pytest_report_header(config):
"""Header for pytest."""
return f"dependencies: numpy-{numpy.__version__}"
@@ -0,0 +1,311 @@
import unittest
from math import pi
import numpy as np
import pytest
from shapely import affinity
from shapely.geometry import Point
from shapely.wkt import loads as load_wkt
class AffineTestCase(unittest.TestCase):
def test_affine_params(self):
g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")
with pytest.raises(TypeError):
affinity.affine_transform(g, None)
with pytest.raises(ValueError):
affinity.affine_transform(g, [1, 2, 3, 4, 5, 6, 7, 8, 9])
with pytest.raises(AttributeError):
affinity.affine_transform(None, [1, 2, 3, 4, 5, 6])
def test_affine_geom_types(self):
# identity matrices, which should result with no transformation
matrix2d = (1, 0, 0, 1, 0, 0)
matrix3d = (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
# empty in, empty out
empty2d = load_wkt("MULTIPOLYGON EMPTY")
assert affinity.affine_transform(empty2d, matrix2d).is_empty
def test_geom(g2, g3=None):
assert not g2.has_z
a2 = affinity.affine_transform(g2, matrix2d)
assert not a2.has_z
assert g2.equals(a2)
if g3 is not None:
assert g3.has_z
a3 = affinity.affine_transform(g3, matrix3d)
assert a3.has_z
assert g3.equals(a3)
return
pt2d = load_wkt("POINT(12.3 45.6)")
pt3d = load_wkt("POINT(12.3 45.6 7.89)")
test_geom(pt2d, pt3d)
ls2d = load_wkt("LINESTRING(0.9 3.4, 0.7 2, 2.5 2.7)")
ls3d = load_wkt("LINESTRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5)")
test_geom(ls2d, ls3d)
lr2d = load_wkt("LINEARRING(0.9 3.4, 0.7 2, 2.5 2.7, 0.9 3.4)")
lr3d = load_wkt("LINEARRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5, 0.9 3.4 3.3)")
test_geom(lr2d, lr3d)
test_geom(
load_wkt(
"POLYGON((0.9 2.3, 0.5 1.1, 2.4 0.8, 0.9 2.3), "
"(1.1 1.7, 0.9 1.3, 1.4 1.2, 1.1 1.7), "
"(1.6 1.3, 1.7 1, 1.9 1.1, 1.6 1.3))"
)
)
test_geom(
load_wkt("MULTIPOINT ((-300 300), (700 300), (-800 -1100), (200 -300))")
)
test_geom(
load_wkt(
"MULTILINESTRING((0 0, -0.7 -0.7, 0.6 -1), "
"(-0.5 0.5, 0.7 0.6, 0 -0.6))"
)
)
test_geom(
load_wkt(
"MULTIPOLYGON(((900 4300, -1100 -400, 900 -800, 900 4300)), "
"((1200 4300, 2300 4400, 1900 1000, 1200 4300)))"
)
)
test_geom(
load_wkt(
"GEOMETRYCOLLECTION(POINT(20 70),"
" POLYGON((60 70, 13 35, 60 -30, 60 70)),"
" LINESTRING(60 70, 50 100, 80 100))"
)
)
def test_affine_2d(self):
g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")
# custom scale and translate
expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)")
matrix2d = (2, 0, 0, 2.5, -5, 4.1)
a2 = affinity.affine_transform(g, matrix2d)
assert a2.equals_exact(expected2d, 1e-6)
assert not a2.has_z
# Make sure a 3D matrix does not make a 3D shape from a 2D input
matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 10, -5, 4.1, 100)
a3 = affinity.affine_transform(g, matrix3d)
assert a3.equals_exact(expected2d, 1e-6)
assert not a3.has_z
def test_affine_3d(self):
g2 = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")
g3 = load_wkt("LINESTRING(2.4 4.1 100.2, 2.4 3 132.8, 3 3 128.6)")
# custom scale and translate
matrix2d = (2, 0, 0, 2.5, -5, 4.1)
matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 0.3048, -5, 4.1, 100)
# Combinations of 2D and 3D geometries and matrices
a22 = affinity.affine_transform(g2, matrix2d)
a23 = affinity.affine_transform(g2, matrix3d)
a32 = affinity.affine_transform(g3, matrix2d)
a33 = affinity.affine_transform(g3, matrix3d)
# Check dimensions
assert not a22.has_z
assert not a23.has_z
assert a32.has_z
assert a33.has_z
# 2D equality checks
expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)")
expected3d = load_wkt(
"LINESTRING(-0.2 14.35 130.54096, " "-0.2 11.6 140.47744, 1 11.6 139.19728)"
)
expected32 = load_wkt(
"LINESTRING(-0.2 14.35 100.2, " "-0.2 11.6 132.8, 1 11.6 128.6)"
)
assert a22.equals_exact(expected2d, 1e-6)
assert a23.equals_exact(expected2d, 1e-6)
# Do explicit 3D check of coordinate values
for a, e in zip(a32.coords, expected32.coords):
for ap, ep in zip(a, e):
self.assertAlmostEqual(ap, ep)
for a, e in zip(a33.coords, expected3d.coords):
for ap, ep in zip(a, e):
self.assertAlmostEqual(ap, ep)
class TransformOpsTestCase(unittest.TestCase):
def test_rotate(self):
ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)")
# counter-clockwise degrees
rls = affinity.rotate(ls, 90)
els = load_wkt("LINESTRING(220 320, 320 320, 320 380)")
assert rls.equals(els)
# retest with named parameters for the same result
rls = affinity.rotate(geom=ls, angle=90, origin="center")
assert rls.equals(els)
# clockwise radians
rls = affinity.rotate(ls, -pi / 2, use_radians=True)
els = load_wkt("LINESTRING(320 380, 220 380, 220 320)")
assert rls.equals(els)
## other `origin` parameters
# around the centroid
rls = affinity.rotate(ls, 90, origin="centroid")
els = load_wkt("LINESTRING(182.5 320, 282.5 320, 282.5 380)")
assert rls.equals(els)
# around the second coordinate tuple
rls = affinity.rotate(ls, 90, origin=ls.coords[1])
els = load_wkt("LINESTRING(140 300, 240 300, 240 360)")
assert rls.equals(els)
# around the absolute Point of origin
rls = affinity.rotate(ls, 90, origin=Point(0, 0))
els = load_wkt("LINESTRING(-400 240, -300 240, -300 300)")
assert rls.equals(els)
def test_rotate_empty(self):
rls = affinity.rotate(load_wkt("LINESTRING EMPTY"), 90)
els = load_wkt("LINESTRING EMPTY")
assert rls.equals(els)
def test_rotate_angle_array(self):
ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)")
els = load_wkt("LINESTRING(220 320, 320 320, 320 380)")
# check with degrees
theta = np.array(90.0)
rls = affinity.rotate(ls, theta)
assert theta.item() == 90.0
assert rls.equals(els)
# check with radians
theta = np.array(pi / 2)
rls = affinity.rotate(ls, theta, use_radians=True)
assert theta.item() == pi / 2
assert rls.equals(els)
def test_scale(self):
ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
# test defaults of 1.0
sls = affinity.scale(ls)
assert sls.equals(ls)
# different scaling in different dimensions
sls = affinity.scale(ls, 2, 3, 0.5)
els = load_wkt("LINESTRING(210 500 5, 210 200 15, 330 200 10)")
assert sls.equals(els)
# Do explicit 3D check of coordinate values
for a, b in zip(sls.coords, els.coords):
for ap, bp in zip(a, b):
self.assertEqual(ap, bp)
# retest with named parameters for the same result
sls = affinity.scale(geom=ls, xfact=2, yfact=3, zfact=0.5, origin="center")
assert sls.equals(els)
## other `origin` parameters
# around the centroid
sls = affinity.scale(ls, 2, 3, 0.5, origin="centroid")
els = load_wkt("LINESTRING(228.75 537.5, 228.75 237.5, 348.75 237.5)")
assert sls.equals(els)
# around the second coordinate tuple
sls = affinity.scale(ls, 2, 3, 0.5, origin=ls.coords[1])
els = load_wkt("LINESTRING(240 600, 240 300, 360 300)")
assert sls.equals(els)
# around some other 3D Point of origin
sls = affinity.scale(ls, 2, 3, 0.5, origin=Point(100, 200, 1000))
els = load_wkt("LINESTRING(380 800 505, 380 500 515, 500 500 510)")
assert sls.equals(els)
# Do explicit 3D check of coordinate values
for a, b in zip(sls.coords, els.coords):
for ap, bp in zip(a, b):
assert ap == bp
def test_scale_empty(self):
sls = affinity.scale(load_wkt("LINESTRING EMPTY"))
els = load_wkt("LINESTRING EMPTY")
assert sls.equals(els)
def test_skew(self):
ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
# test default shear angles of 0.0
sls = affinity.skew(ls)
assert sls.equals(ls)
# different shearing in x- and y-directions
sls = affinity.skew(ls, 15, -30)
els = load_wkt(
"LINESTRING (253.39745962155615 417.3205080756888, "
"226.60254037844385 317.3205080756888, "
"286.60254037844385 282.67949192431126)"
)
assert sls.equals_exact(els, 1e-6)
# retest with radians for the same result
sls = affinity.skew(ls, pi / 12, -pi / 6, use_radians=True)
assert sls.equals_exact(els, 1e-6)
# retest with named parameters for the same result
sls = affinity.skew(geom=ls, xs=15, ys=-30, origin="center", use_radians=False)
assert sls.equals_exact(els, 1e-6)
## other `origin` parameters
# around the centroid
sls = affinity.skew(ls, 15, -30, origin="centroid")
els = load_wkt(
"LINESTRING(258.42150697963973 406.49519052838332, "
"231.6265877365273980 306.4951905283833185, "
"291.6265877365274264 271.8541743770057337)"
)
assert sls.equals_exact(els, 1e-6)
# around the second coordinate tuple
sls = affinity.skew(ls, 15, -30, origin=ls.coords[1])
els = load_wkt(
"LINESTRING(266.7949192431123038 400, 240 300, " "300 265.3589838486224153)"
)
assert sls.equals_exact(els, 1e-6)
# around the absolute Point of origin
sls = affinity.skew(ls, 15, -30, origin=Point(0, 0))
els = load_wkt(
"LINESTRING(347.179676972449101 261.435935394489832, "
"320.3847577293367976 161.4359353944898317, "
"380.3847577293367976 126.7949192431122754)"
)
assert sls.equals_exact(els, 1e-6)
def test_skew_empty(self):
sls = affinity.skew(load_wkt("LINESTRING EMPTY"))
els = load_wkt("LINESTRING EMPTY")
assert sls.equals(els)
def test_skew_xs_ys_array(self):
ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
els = load_wkt(
"LINESTRING (253.39745962155615 417.3205080756888, "
"226.60254037844385 317.3205080756888, "
"286.60254037844385 282.67949192431126)"
)
# check with degrees
xs_ys = np.array([15.0, -30.0])
sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...])
assert xs_ys[0] == 15.0
assert xs_ys[1] == -30.0
assert sls.equals_exact(els, 1e-6)
# check with radians
xs_ys = np.array([pi / 12, -pi / 6])
sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...], use_radians=True)
assert xs_ys[0] == pi / 12
assert xs_ys[1] == -pi / 6
assert sls.equals_exact(els, 1e-6)
def test_translate(self):
ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
# test default offset of 0.0
tls = affinity.translate(ls)
assert tls.equals(ls)
# test all offsets
tls = affinity.translate(ls, 100, 400, -10)
els = load_wkt("LINESTRING(340 800 0, 340 700 20, 400 700 10)")
assert tls.equals(els)
# Do explicit 3D check of coordinate values
for a, b in zip(tls.coords, els.coords):
for ap, bp in zip(a, b):
assert ap == bp
# retest with named parameters for the same result
tls = affinity.translate(geom=ls, xoff=100, yoff=400, zoff=-10)
assert tls.equals(els)
def test_translate_empty(self):
tls = affinity.translate(load_wkt("LINESTRING EMPTY"))
els = load_wkt("LINESTRING EMPTY")
self.assertTrue(tls.equals(els))
assert tls.equals(els)
@@ -0,0 +1,20 @@
import unittest
from shapely import geometry
class BoxTestCase(unittest.TestCase):
def test_ccw(self):
b = geometry.box(0, 0, 1, 1, ccw=True)
assert b.exterior.coords[0] == (1.0, 0.0)
assert b.exterior.coords[1] == (1.0, 1.0)
def test_ccw_default(self):
b = geometry.box(0, 0, 1, 1)
assert b.exterior.coords[0] == (1.0, 0.0)
assert b.exterior.coords[1] == (1.0, 1.0)
def test_cw(self):
b = geometry.box(0, 0, 1, 1, ccw=False)
assert b.exterior.coords[0] == (0.0, 0.0)
assert b.exterior.coords[1] == (0.0, 1.0)
@@ -0,0 +1,172 @@
import unittest
import pytest
from shapely import geometry
from shapely.constructive import BufferCapStyle, BufferJoinStyle
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
@pytest.mark.parametrize("distance", [float("nan"), float("inf")])
def test_non_finite_distance(distance):
g = geometry.Point(0, 0)
with pytest.raises(ValueError, match="distance must be finite"):
g.buffer(distance)
class BufferTests(unittest.TestCase):
"""Test Buffer Point/Line/Polygon with and without single_sided params"""
def test_empty(self):
g = geometry.Point(0, 0)
h = g.buffer(0)
assert h.is_empty
def test_point(self):
g = geometry.Point(0, 0)
h = g.buffer(1, quad_segs=1)
assert h.geom_type == "Polygon"
expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_point_single_sidedd(self):
g = geometry.Point(0, 0)
h = g.buffer(1, quad_segs=1, single_sided=True)
assert h.geom_type == "Polygon"
expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_line(self):
g = geometry.LineString([[0, 0], [0, 1]])
h = g.buffer(1, quad_segs=1)
assert h.geom_type == "Polygon"
expected_coord = [
(-1.0, 1.0),
(0, 2.0),
(1.0, 1.0),
(1.0, 0.0),
(0, -1.0),
(-1.0, 0.0),
(-1.0, 1.0),
]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_line_single_sideded_left(self):
g = geometry.LineString([[0, 0], [0, 1]])
h = g.buffer(1, quad_segs=1, single_sided=True)
assert h.geom_type == "Polygon"
expected_coord = [(0.0, 1.0), (0.0, 0.0), (-1.0, 0.0), (-1.0, 1.0), (0.0, 1.0)]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_line_single_sideded_right(self):
g = geometry.LineString([[0, 0], [0, 1]])
h = g.buffer(-1, quad_segs=1, single_sided=True)
assert h.geom_type == "Polygon"
expected_coord = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_polygon(self):
g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
h = g.buffer(1, quad_segs=1)
assert h.geom_type == "Polygon"
expected_coord = [
(-1.0, 0.0),
(-1.0, 1.0),
(0.0, 2.0),
(1.0, 2.0),
(2.0, 1.0),
(2.0, 0.0),
(1.0, -1.0),
(0.0, -1.0),
(-1.0, 0.0),
]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_polygon_single_sideded(self):
g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
h = g.buffer(1, quad_segs=1, single_sided=True)
assert h.geom_type == "Polygon"
expected_coord = [
(-1.0, 0.0),
(-1.0, 1.0),
(0.0, 2.0),
(1.0, 2.0),
(2.0, 1.0),
(2.0, 0.0),
(1.0, -1.0),
(0.0, -1.0),
(-1.0, 0.0),
]
for index, coord in enumerate(h.exterior.coords):
assert coord[0] == pytest.approx(expected_coord[index][0])
assert coord[1] == pytest.approx(expected_coord[index][1])
def test_enum_values(self):
assert CAP_STYLE.round == 1
assert CAP_STYLE.round == BufferCapStyle.round
assert CAP_STYLE.flat == 2
assert CAP_STYLE.flat == BufferCapStyle.flat
assert CAP_STYLE.square == 3
assert CAP_STYLE.square == BufferCapStyle.square
assert JOIN_STYLE.round == 1
assert JOIN_STYLE.round == BufferJoinStyle.round
assert JOIN_STYLE.mitre == 2
assert JOIN_STYLE.mitre == BufferJoinStyle.mitre
assert JOIN_STYLE.bevel == 3
assert JOIN_STYLE.bevel == BufferJoinStyle.bevel
def test_cap_style(self):
g = geometry.LineString([[0, 0], [1, 0]])
h = g.buffer(1, cap_style=BufferCapStyle.round)
assert h == g.buffer(1, cap_style=CAP_STYLE.round)
assert h == g.buffer(1, cap_style="round")
h = g.buffer(1, cap_style=BufferCapStyle.flat)
assert h == g.buffer(1, cap_style=CAP_STYLE.flat)
assert h == g.buffer(1, cap_style="flat")
h = g.buffer(1, cap_style=BufferCapStyle.square)
assert h == g.buffer(1, cap_style=CAP_STYLE.square)
assert h == g.buffer(1, cap_style="square")
def test_buffer_style(self):
g = geometry.LineString([[0, 0], [1, 0]])
h = g.buffer(1, join_style=BufferJoinStyle.round)
assert h == g.buffer(1, join_style=JOIN_STYLE.round)
assert h == g.buffer(1, join_style="round")
h = g.buffer(1, join_style=BufferJoinStyle.mitre)
assert h == g.buffer(1, join_style=JOIN_STYLE.mitre)
assert h == g.buffer(1, join_style="mitre")
h = g.buffer(1, join_style=BufferJoinStyle.bevel)
assert h == g.buffer(1, join_style=JOIN_STYLE.bevel)
assert h == g.buffer(1, join_style="bevel")
def test_deprecated_quadsegs():
point = geometry.Point(0, 0)
with pytest.warns(FutureWarning):
result = point.buffer(1, quadsegs=1)
expected = point.buffer(1, quad_segs=1)
assert result.equals(expected)
def test_resolution_alias():
point = geometry.Point(0, 0)
result = point.buffer(1, resolution=1)
expected = point.buffer(1, quad_segs=1)
assert result.equals(expected)
@@ -0,0 +1,51 @@
import unittest
import pytest
from shapely.geometry.polygon import LinearRing, orient, Polygon, signed_area
class SignedAreaTestCase(unittest.TestCase):
def test_triangle(self):
tri = LinearRing([(0, 0), (2, 5), (7, 0)])
assert signed_area(tri) == pytest.approx(-7 * 5 / 2)
def test_square(self):
xmin, xmax = (-1, 1)
ymin, ymax = (-2, 3)
rect = LinearRing(
[(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)]
)
assert signed_area(rect) == pytest.approx(10.0)
class RingOrientationTestCase(unittest.TestCase):
def test_ccw(self):
ring = LinearRing([(1, 0), (0, 1), (0, 0)])
assert ring.is_ccw
def test_cw(self):
ring = LinearRing([(0, 0), (0, 1), (1, 0)])
assert not ring.is_ccw
class PolygonOrienterTestCase(unittest.TestCase):
def test_no_holes(self):
ring = LinearRing([(0, 0), (0, 1), (1, 0)])
polygon = Polygon(ring)
assert not polygon.exterior.is_ccw
polygon = orient(polygon, 1)
assert polygon.exterior.is_ccw
def test_holes(self):
# fmt: off
polygon = Polygon(
[(0, 0), (0, 1), (1, 0)],
[[(0.5, 0.25), (0.25, 0.5), (0.25, 0.25)]]
)
# fmt: on
assert not polygon.exterior.is_ccw
assert polygon.interiors[0].is_ccw
polygon = orient(polygon, 1)
assert polygon.exterior.is_ccw
assert not polygon.interiors[0].is_ccw
@@ -0,0 +1,120 @@
"""
Tests for GEOSClipByRect based on unit tests from libgeos.
There are some expected differences due to Shapely's handling of empty
geometries.
"""
import pytest
from shapely.ops import clip_by_rect
from shapely.wkt import dumps as dump_wkt
from shapely.wkt import loads as load_wkt
def test_point_outside():
"""Point outside"""
geom1 = load_wkt("POINT (0 0)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def test_point_inside():
"""Point inside"""
geom1 = load_wkt("POINT (15 15)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "POINT (15 15)"
def test_point_on_boundary():
"""Point on boundary"""
geom1 = load_wkt("POINT (15 10)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def test_line_outside():
"""Line outside"""
geom1 = load_wkt("LINESTRING (0 0, -5 5)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def test_line_inside():
"""Line inside"""
geom1 = load_wkt("LINESTRING (15 15, 16 15)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "LINESTRING (15 15, 16 15)"
def test_line_on_boundary():
"""Line on boundary"""
geom1 = load_wkt("LINESTRING (10 15, 10 10, 15 10)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def test_line_splitting_rectangle():
"""Line splitting rectangle"""
geom1 = load_wkt("LINESTRING (10 5, 25 20)")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "LINESTRING (15 10, 20 15)"
@pytest.mark.xfail(reason="TODO issue to CCW")
def test_polygon_shell_ccw_fully_on_rectangle_boundary():
"""Polygon shell (CCW) fully on rectangle boundary"""
geom1 = load_wkt("POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert (
dump_wkt(geom2, rounding_precision=0)
== "POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))"
)
@pytest.mark.xfail(reason="TODO issue to CW")
def test_polygon_shell_cc_fully_on_rectangle_boundary():
"""Polygon shell (CW) fully on rectangle boundary"""
geom1 = load_wkt("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert (
dump_wkt(geom2, rounding_precision=0)
== "POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))"
)
def polygon_hole_ccw_fully_on_rectangle_boundary():
"""Polygon hole (CCW) fully on rectangle boundary"""
geom1 = load_wkt(
"POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 20 10, 20 20, 10 20, 10 10))"
)
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def polygon_hole_cw_fully_on_rectangle_boundary():
"""Polygon hole (CW) fully on rectangle boundary"""
geom1 = load_wkt(
"POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 10 20, 20 20, 20 10, 10 10))"
)
geom2 = clip_by_rect(geom1, 10, 10, 20, 20)
assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY"
def polygon_fully_within_rectangle():
"""Polygon fully within rectangle"""
wkt = "POLYGON ((1 1, 1 30, 30 30, 30 1, 1 1), (10 10, 20 10, 20 20, 10 20, 10 10))"
geom1 = load_wkt(wkt)
geom2 = clip_by_rect(geom1, 0, 0, 40, 40)
assert dump_wkt(geom2, rounding_precision=0) == wkt
def polygon_overlapping_rectangle():
"""Polygon overlapping rectangle"""
wkt = "POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 20 10, 20 20, 10 20, 10 10))"
geom1 = load_wkt(wkt)
geom2 = clip_by_rect(geom1, 5, 5, 15, 15)
assert (
dump_wkt(geom2, rounding_precision=0)
== "POLYGON ((5 5, 5 15, 10 15, 10 10, 15 10, 15 5, 5 5))"
)
@@ -0,0 +1,214 @@
"""
When a "context" passed to shape/asShape has a coordinate
which is missing a dimension we should raise a descriptive error.
When we use mixed dimensions in a WKT geometry, the parser strips
any dimension which is not present in every coordinate.
"""
import pytest
from shapely import wkt
from shapely.errors import GEOSException
from shapely.geometry import LineString, Polygon, shape
from shapely.geos import geos_version
geojson_cases = [
{"type": "LineString", "coordinates": [[1, 1, 1], [2, 2]]},
# Specific test case from #869
{
"type": "Polygon",
"coordinates": [
[
[55.12916764533149, 24.980385694214384, 2.5],
[55.13098248044217, 24.979828079961905],
[55.13966519231666, 24.97801442415322],
[55.13966563924936, 24.97801442415322],
[55.14139286840762, 24.982307444496097],
[55.14169331277646, 24.983717465495562],
[55.14203489144224, 24.985419446276566, 2.5],
[55.14180327151276, 24.98428602667792, 2.5],
[55.14170091915952, 24.984242720177235, 2.5],
[55.14122966992623, 24.984954809433702, 2.5],
[55.14134021791831, 24.985473928648396, 2.5],
[55.141405876161286, 24.986090184809793, 2.5],
[55.141361358941225, 24.986138101357326, 2.5],
[55.14093322994411, 24.986218753894093, 2.5],
[55.140897653420964, 24.986214283545635, 2.5],
[55.14095492976058, 24.9863027591922, 2.5],
[55.140900447388745, 24.98628436557094, 2.5],
[55.140867059473706, 24.98628869622101, 2.5],
[55.14089155325796, 24.986402364143782, 2.5],
[55.14090938808566, 24.986479011993385, 2.5],
[55.140943893587824, 24.986471188883584, 2.5],
[55.1410161176551, 24.9864174050037, 2.5],
[55.140996932409635, 24.986521806266644, 2.5],
[55.14163554031332, 24.986910400619593, 2.5],
[55.14095781686062, 24.987033474900578, 2.5],
[55.14058258698692, 24.98693261266349, 2.5],
[55.14032624044253, 24.98747538747211, 2.5],
[55.14007240846915, 24.988001119077232, 2.5],
[55.14013122149105, 24.98831115636925, 2.5],
[55.13991827457961, 24.98834356639557, 2.5],
[55.139779460946755, 24.988254625087706, 2.5],
[55.13974742344948, 24.988261377176524, 2.5],
[55.139515198160304, 24.98841811876934, 2.5],
[55.13903617238334, 24.98817914139135, 2.5],
[55.1391330764994, 24.988660542040925, 2.5],
[55.13914369357698, 24.989438289540374, 2.5],
[55.136431216517785, 24.98966711550207, 2.0],
[55.13659028641709, 24.99041706302204, 2.0],
[55.1355852030721, 24.990933481401207, 2.5],
[55.13535549235394, 24.99110470506038, 2.5],
[55.13512578163577, 24.99127592871955, 2.5],
[55.129969653784556, 24.991440074326995, 2.5],
[55.130221623112746, 24.988070688875112, 2.5],
[55.130451333830905, 24.98789946521594, 2.5],
[55.13089208224919, 24.98742639990359, 2.5],
[55.132177586827666, 24.989003408454433, 2.5],
[55.13238862452779, 24.988701566801254, 2.5],
[55.132482594977674, 24.988501518707757, 2.5],
[55.132525994610624, 24.988048802794115, 2.5],
[55.13249018525683, 24.987180623870653, 2.5],
[55.13253358488978, 24.986727907957015, 2.5],
[55.1322761673244, 24.985827132742713, 2.5],
[55.13163341503516, 24.98503862846729, 2.5],
[55.131514764536504, 24.984469124700183, 2.5],
[55.131275600894, 24.983796337257242, 2.0],
[55.13066865795855, 24.98387601190528, 2.0],
[55.13026930682963, 24.981537228037503, 2.0],
[55.130260412698846, 24.981495691049748, 2.0],
[55.13025151856806, 24.981454154061993, 2.0],
[55.13022925995803, 24.98096497686874, 2.5],
[55.12984453059386, 24.9804285816199, 2.5],
[55.129998291954365, 24.98021419115843, 2.5],
[55.12916764533149, 24.980385694214384, 2.5],
]
],
},
]
direct_cases = [
(LineString, [[[0, 0, 0], [1, 1]]]),
(Polygon, [[[0, 0, 0], [1, 1, 0], [1, 1], [0, 1, 0], [0, 0, 0]]]),
# Specific test case from #869
(
Polygon,
[
[
[55.12916764533149, 24.980385694214384, 2.5],
[55.13098248044217, 24.979828079961905],
[55.13966519231666, 24.97801442415322],
[55.13966563924936, 24.97801442415322],
[55.14139286840762, 24.982307444496097],
[55.14169331277646, 24.983717465495562],
[55.14203489144224, 24.985419446276566, 2.5],
[55.14180327151276, 24.98428602667792, 2.5],
[55.14170091915952, 24.984242720177235, 2.5],
[55.14122966992623, 24.984954809433702, 2.5],
[55.14134021791831, 24.985473928648396, 2.5],
[55.141405876161286, 24.986090184809793, 2.5],
[55.141361358941225, 24.986138101357326, 2.5],
[55.14093322994411, 24.986218753894093, 2.5],
[55.140897653420964, 24.986214283545635, 2.5],
[55.14095492976058, 24.9863027591922, 2.5],
[55.140900447388745, 24.98628436557094, 2.5],
[55.140867059473706, 24.98628869622101, 2.5],
[55.14089155325796, 24.986402364143782, 2.5],
[55.14090938808566, 24.986479011993385, 2.5],
[55.140943893587824, 24.986471188883584, 2.5],
[55.1410161176551, 24.9864174050037, 2.5],
[55.140996932409635, 24.986521806266644, 2.5],
[55.14163554031332, 24.986910400619593, 2.5],
[55.14095781686062, 24.987033474900578, 2.5],
[55.14058258698692, 24.98693261266349, 2.5],
[55.14032624044253, 24.98747538747211, 2.5],
[55.14007240846915, 24.988001119077232, 2.5],
[55.14013122149105, 24.98831115636925, 2.5],
[55.13991827457961, 24.98834356639557, 2.5],
[55.139779460946755, 24.988254625087706, 2.5],
[55.13974742344948, 24.988261377176524, 2.5],
[55.139515198160304, 24.98841811876934, 2.5],
[55.13903617238334, 24.98817914139135, 2.5],
[55.1391330764994, 24.988660542040925, 2.5],
[55.13914369357698, 24.989438289540374, 2.5],
[55.136431216517785, 24.98966711550207, 2.0],
[55.13659028641709, 24.99041706302204, 2.0],
[55.1355852030721, 24.990933481401207, 2.5],
[55.13535549235394, 24.99110470506038, 2.5],
[55.13512578163577, 24.99127592871955, 2.5],
[55.129969653784556, 24.991440074326995, 2.5],
[55.130221623112746, 24.988070688875112, 2.5],
[55.130451333830905, 24.98789946521594, 2.5],
[55.13089208224919, 24.98742639990359, 2.5],
[55.132177586827666, 24.989003408454433, 2.5],
[55.13238862452779, 24.988701566801254, 2.5],
[55.132482594977674, 24.988501518707757, 2.5],
[55.132525994610624, 24.988048802794115, 2.5],
[55.13249018525683, 24.987180623870653, 2.5],
[55.13253358488978, 24.986727907957015, 2.5],
[55.1322761673244, 24.985827132742713, 2.5],
[55.13163341503516, 24.98503862846729, 2.5],
[55.131514764536504, 24.984469124700183, 2.5],
[55.131275600894, 24.983796337257242, 2.0],
[55.13066865795855, 24.98387601190528, 2.0],
[55.13026930682963, 24.981537228037503, 2.0],
[55.130260412698846, 24.981495691049748, 2.0],
[55.13025151856806, 24.981454154061993, 2.0],
[55.13022925995803, 24.98096497686874, 2.5],
[55.12984453059386, 24.9804285816199, 2.5],
[55.129998291954365, 24.98021419115843, 2.5],
[55.12916764533149, 24.980385694214384, 2.5],
]
],
),
]
wkt_cases = [
# preserve 3rd dimension
("MULTIPOINT (1 1 1, 2 2)", "MULTIPOINT Z (1 1 1, 2 2 0)"),
("MULTIPOINT (1 1, 2 2 2)", "MULTIPOINT Z (1 1 0, 2 2 2)"),
("LINESTRING (1 1 1, 2 2)", "LINESTRING Z (1 1 1, 2 2 0)"),
(
"POLYGON ((0 0 0, 1 0 0, 1 1, 0 1 0, 0 0 0))",
"POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0))",
),
# drop 3rd dimension
("LINESTRING (1 1, 2 2 2)", "LINESTRING (1 1, 2 2)"),
("POLYGON ((0 0, 1 0 1, 1 1, 0 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
]
@pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:")
@pytest.mark.parametrize("geojson", geojson_cases)
def test_create_from_geojson(geojson):
# exact error depends on numpy version
with pytest.raises((ValueError, TypeError)) as exc:
shape(geojson).wkt
assert exc.match(
"Inconsistent coordinate dimensionality|Input operand 0 does not have enough dimensions|ufunc 'linestrings' not supported for the input types|setting an array element with a sequence. The requested array has an inhomogeneous shape"
)
@pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:")
@pytest.mark.parametrize("constructor, args", direct_cases)
def test_create_directly(constructor, args):
with pytest.raises((ValueError, TypeError)) as exc:
constructor(*args)
assert exc.match(
"Inconsistent coordinate dimensionality|Input operand 0 does not have enough dimensions|ufunc 'linestrings' not supported for the input types|setting an array element with a sequence. The requested array has an inhomogeneous shape"
)
@pytest.mark.parametrize("wkt_geom,expected", wkt_cases)
def test_create_from_wkt(wkt_geom, expected):
if geos_version >= (3, 12, 0):
# https://github.com/shapely/shapely/issues/1541
with pytest.raises(GEOSException):
wkt.loads(wkt_geom)
else:
geom = wkt.loads(wkt_geom)
assert geom.wkt == expected
@@ -0,0 +1,32 @@
import unittest
from shapely.geometry import LineString, Point, Polygon
from shapely.ops import triangulate
class DelaunayTriangulation(unittest.TestCase):
"""
Only testing the number of triangles and their type here.
This doesn't actually test the points in the resulting geometries.
"""
def setUp(self):
self.p = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
def test_polys(self):
polys = triangulate(self.p)
assert len(polys) == 2
for p in polys:
assert isinstance(p, Polygon)
def test_lines(self):
polys = triangulate(self.p, edges=True)
assert len(polys) == 5
for p in polys:
assert isinstance(p, LineString)
def test_point(self):
p = Point(1, 1)
polys = triangulate(p)
assert len(polys) == 0
@@ -0,0 +1,22 @@
from shapely.geometry import MultiPolygon, Point, Polygon
def test_empty_polygon():
"""No constructor arg makes an empty polygon geometry."""
assert Polygon().is_empty
def test_empty_multipolygon():
"""No constructor arg makes an empty multipolygon geometry."""
assert MultiPolygon().is_empty
def test_multipolygon_empty_polygon():
"""An empty polygon passed to MultiPolygon() makes an empty
multipolygon geometry."""
assert MultiPolygon([Polygon()]).is_empty
def test_multipolygon_empty_among_polygon():
"""An empty polygon passed to MultiPolygon() is ignored."""
assert len(MultiPolygon([Point(0, 0).buffer(1.0), Polygon()]).geoms) == 1
@@ -0,0 +1,32 @@
import pytest
from shapely import Point
from shapely.errors import ShapelyDeprecationWarning
def test_equals_exact():
p1 = Point(1.0, 1.0)
p2 = Point(2.0, 2.0)
assert not p1.equals(p2)
assert not p1.equals_exact(p2, 0.001)
assert p1.equals_exact(p2, 1.42)
def test_almost_equals_default():
p1 = Point(1.0, 1.0)
p2 = Point(1.0 + 1e-7, 1.0 + 1e-7) # almost equal to 6 places
p3 = Point(1.0 + 1e-6, 1.0 + 1e-6) # not almost equal
with pytest.warns(ShapelyDeprecationWarning):
assert p1.almost_equals(p2)
with pytest.warns(ShapelyDeprecationWarning):
assert not p1.almost_equals(p3)
def test_almost_equals():
p1 = Point(1.0, 1.0)
p2 = Point(1.1, 1.1)
assert not p1.equals(p2)
with pytest.warns(ShapelyDeprecationWarning):
assert p1.almost_equals(p2, 0)
with pytest.warns(ShapelyDeprecationWarning):
assert not p1.almost_equals(p2, 1)
@@ -0,0 +1,118 @@
import unittest
from shapely import wkt
from shapely.geometry import shape
from shapely.geometry.linestring import LineString
from shapely.geometry.multilinestring import MultiLineString
from shapely.geometry.multipoint import MultiPoint
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry.polygon import LinearRing, Polygon
class GeoThing:
def __init__(self, d):
self.__geo_interface__ = d
class GeoInterfaceTestCase(unittest.TestCase):
def test_geointerface(self):
# Convert a dictionary
d = {"type": "Point", "coordinates": (0.0, 0.0)}
geom = shape(d)
assert geom.geom_type == "Point"
assert tuple(geom.coords) == ((0.0, 0.0),)
# Convert an object that implements the geo protocol
geom = None
thing = GeoThing({"type": "Point", "coordinates": (0.0, 0.0)})
geom = shape(thing)
assert geom.geom_type == "Point"
assert tuple(geom.coords) == ((0.0, 0.0),)
# Check line string
geom = shape({"type": "LineString", "coordinates": ((-1.0, -1.0), (1.0, 1.0))})
assert isinstance(geom, LineString)
assert tuple(geom.coords) == ((-1.0, -1.0), (1.0, 1.0))
# Check linearring
geom = shape(
{
"type": "LinearRing",
"coordinates": (
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(2.0, -1.0),
(0.0, 0.0),
),
}
)
assert isinstance(geom, LinearRing)
assert tuple(geom.coords) == (
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(2.0, -1.0),
(0.0, 0.0),
)
# polygon
geom = shape(
{
"type": "Polygon",
"coordinates": (
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0), (0.0, 0.0)),
((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)),
),
}
)
assert isinstance(geom, Polygon)
assert tuple(geom.exterior.coords) == (
(0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(2.0, -1.0),
(0.0, 0.0),
)
assert len(geom.interiors) == 1
# multi point
geom = shape({"type": "MultiPoint", "coordinates": ((1.0, 2.0), (3.0, 4.0))})
assert isinstance(geom, MultiPoint)
assert len(geom.geoms) == 2
# multi line string
geom = shape(
{"type": "MultiLineString", "coordinates": (((0.0, 0.0), (1.0, 2.0)),)}
)
assert isinstance(geom, MultiLineString)
assert len(geom.geoms) == 1
# multi polygon
geom = shape(
{
"type": "MultiPolygon",
"coordinates": [
(
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),
((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)),
)
],
}
)
assert isinstance(geom, MultiPolygon)
assert len(geom.geoms) == 1
def test_empty_wkt_polygon():
"""Confirm fix for issue #450"""
g = wkt.loads("POLYGON EMPTY")
assert g.__geo_interface__["type"] == "Polygon"
assert g.__geo_interface__["coordinates"] == ()
def test_empty_polygon():
"""Confirm fix for issue #450"""
g = Polygon()
assert g.__geo_interface__["type"] == "Polygon"
assert g.__geo_interface__["coordinates"] == ()
@@ -0,0 +1,28 @@
"""Test recovery from operation on invalid geometries
"""
import unittest
import pytest
import shapely
from shapely.errors import TopologicalError
from shapely.geometry import Polygon
class InvalidGeometriesTestCase(unittest.TestCase):
def test_invalid_intersection(self):
# Make a self-intersecting polygon
polygon_invalid = Polygon([(0, 0), (1, 1), (1, -1), (0, 1), (0, 0)])
assert not polygon_invalid.is_valid
# Intersect with a valid polygon
polygon = Polygon([(-0.5, -0.5), (-0.5, 0.5), (0.5, 0.5), (0.5, -5)])
assert polygon.is_valid
assert polygon_invalid.intersects(polygon)
with pytest.raises((TopologicalError, shapely.GEOSException)):
polygon_invalid.intersection(polygon)
with pytest.raises((TopologicalError, shapely.GEOSException)):
polygon.intersection(polygon_invalid)
return
@@ -0,0 +1,72 @@
import unittest
import pytest
import shapely
from shapely.geometry import LineString, MultiLineString, Point
class LinearReferencingTestCase(unittest.TestCase):
def setUp(self):
self.point = Point(1, 1)
self.line1 = LineString([(0, 0), (2, 0)])
self.line2 = LineString([(3, 0), (3, 6)])
self.multiline = MultiLineString(
[list(self.line1.coords), list(self.line2.coords)]
)
def test_line1_project(self):
assert self.line1.project(self.point) == 1.0
assert self.line1.project(self.point, normalized=True) == 0.5
def test_alias_project(self):
assert self.line1.line_locate_point(self.point) == 1.0
assert self.line1.line_locate_point(self.point, normalized=True) == 0.5
def test_line2_project(self):
assert self.line2.project(self.point) == 1.0
assert self.line2.project(self.point, normalized=True) == pytest.approx(
0.16666666666, 8
)
def test_multiline_project(self):
assert self.multiline.project(self.point) == 1.0
assert self.multiline.project(self.point, normalized=True) == 0.125
def test_not_supported_project(self):
with pytest.raises(shapely.GEOSException, match="IllegalArgumentException"):
self.point.buffer(1.0).project(self.point)
def test_not_on_line_project(self):
# Points that aren't on the line project to 0.
assert self.line1.project(Point(-10, -10)) == 0.0
def test_line1_interpolate(self):
assert self.line1.interpolate(0.5).equals(Point(0.5, 0.0))
assert self.line1.interpolate(-0.5).equals(Point(1.5, 0.0))
assert self.line1.interpolate(0.5, normalized=True).equals(Point(1, 0))
assert self.line1.interpolate(-0.5, normalized=True).equals(Point(1, 0))
def test_alias_interpolate(self):
assert self.line1.line_interpolate_point(0.5).equals(Point(0.5, 0.0))
assert self.line1.line_interpolate_point(-0.5).equals(Point(1.5, 0.0))
assert self.line1.line_interpolate_point(0.5, normalized=True).equals(
Point(1, 0)
)
assert self.line1.line_interpolate_point(-0.5, normalized=True).equals(
Point(1, 0)
)
def test_line2_interpolate(self):
assert self.line2.interpolate(0.5).equals(Point(3.0, 0.5))
assert self.line2.interpolate(0.5, normalized=True).equals(Point(3, 3))
def test_multiline_interpolate(self):
assert self.multiline.interpolate(0.5).equals(Point(0.5, 0))
assert self.multiline.interpolate(0.5, normalized=True).equals(Point(3.0, 2.0))
def test_line_ends_interpolate(self):
# Distances greater than length of the line or less than
# zero yield the line's ends.
assert self.line1.interpolate(-1000).equals(Point(0.0, 0.0))
assert self.line1.interpolate(1000).equals(Point(2.0, 0.0))
@@ -0,0 +1,44 @@
import unittest
from shapely.geometry import LineString, MultiLineString
from shapely.ops import linemerge
class LineMergeTestCase(unittest.TestCase):
def test_linemerge(self):
lines = MultiLineString([[(0, 0), (1, 1)], [(2, 0), (2, 1), (1, 1)]])
result = linemerge(lines)
assert isinstance(result, LineString)
assert not result.is_ring
assert len(result.coords) == 4
assert result.coords[0] == (0.0, 0.0)
assert result.coords[3] == (2.0, 0.0)
lines2 = MultiLineString([((0, 0), (1, 1)), ((0, 0), (2, 0), (2, 1), (1, 1))])
result = linemerge(lines2)
assert result.is_ring
assert len(result.coords) == 5
lines3 = [
LineString([(0, 0), (1, 1)]),
LineString([(0, 0), (0, 1)]),
]
result = linemerge(lines3)
assert not result.is_ring
assert len(result.coords) == 3
assert result.coords[0] == (0.0, 1.0)
assert result.coords[2] == (1.0, 1.0)
lines4 = [
[(0, 0), (1, 1)],
[(0, 0), (0, 1)],
]
assert result.equals(linemerge(lines4))
lines5 = [
((0, 0), (1, 1)),
((1, 0), (0, 1)),
]
result = linemerge(lines5)
assert result.geom_type == "MultiLineString"
@@ -0,0 +1,56 @@
"""Test locale independence of WKT
"""
import locale
import sys
import unittest
from shapely.wkt import dumps, loads
# Set locale to one that uses a comma as decimal separator
# TODO: try a few other common locales
if sys.platform == "win32":
test_locales = {"Portuguese": "portuguese_brazil", "Italian": "italian_italy"}
else:
test_locales = {
"Portuguese": "pt_BR.UTF-8",
"Italian": "it_IT.UTF-8",
}
do_test_locale = False
def setUpModule():
global do_test_locale
for name in test_locales:
try:
test_locale = test_locales[name]
locale.setlocale(locale.LC_ALL, test_locale)
do_test_locale = True
break
except Exception:
pass
if not do_test_locale:
raise unittest.SkipTest("test locale not found")
def tearDownModule():
if sys.platform == "win32" or sys.version_info[0:2] >= (3, 11):
locale.setlocale(locale.LC_ALL, "")
else:
# Deprecated since version 3.11, will be removed in version 3.13
locale.resetlocale()
class LocaleTestCase(unittest.TestCase):
# @unittest.skipIf(not do_test_locale, 'test locale not found')
def test_wkt_locale(self):
# Test reading and writing
p = loads("POINT (0.0 0.0)")
assert p.x == 0.0
assert p.y == 0.0
wkt = dumps(p)
assert wkt.startswith("POINT")
assert "," not in wkt
@@ -0,0 +1,18 @@
from shapely.geometry import Polygon
from shapely.tests.legacy.conftest import requires_geos_38
from shapely.validation import make_valid
@requires_geos_38
def test_make_valid_invalid_input():
geom = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)])
valid = make_valid(geom)
assert len(valid.geoms) == 2
assert all(geom.geom_type == "Polygon" for geom in valid.geoms)
@requires_geos_38
def test_make_valid_input():
geom = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
valid = make_valid(geom)
assert id(valid) == id(geom)
@@ -0,0 +1,14 @@
import unittest
from shapely.geometry import mapping, Point, Polygon
class MappingTestCase(unittest.TestCase):
def test_point(self):
m = mapping(Point(0, 0))
assert m["type"] == "Point"
assert m["coordinates"] == (0.0, 0.0)
def test_empty_polygon(self):
"""Empty polygons will round trip without error"""
assert mapping(Polygon()) is not None
@@ -0,0 +1,40 @@
"""
Tests for the minimum clearance property.
"""
import math
import pytest
from shapely.geos import geos_version
from shapely.wkt import loads as load_wkt
requires_geos_36 = pytest.mark.skipif(
geos_version < (3, 6, 0), reason="GEOS >= 3.6.0 is required."
)
@requires_geos_36
def test_point():
point = load_wkt("POINT (0 0)")
assert point.minimum_clearance == math.inf
@requires_geos_36
def test_linestring():
line = load_wkt("LINESTRING (0 0, 1 1, 2 2)")
assert round(line.minimum_clearance, 6) == 1.414214
@requires_geos_36
def test_simple_polygon():
poly = load_wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
assert poly.minimum_clearance == 1.0
@requires_geos_36
def test_more_complicated_polygon():
poly = load_wkt(
"POLYGON ((20 20, 34 124, 70 140, 130 130, 70 100, 110 70, 170 20, 90 10, 20 20))"
)
assert round(poly.minimum_clearance, 6) == 35.777088
@@ -0,0 +1,42 @@
# Tests of support for Numpy ndarrays. See
# https://github.com/sgillies/shapely/issues/26 for discussion.
import unittest
from functools import reduce
import numpy as np
from shapely import geometry
class TransposeTestCase(unittest.TestCase):
def test_multipoint(self):
arr = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]])
tarr = arr.T
shape = geometry.MultiPoint(tarr)
coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape.geoms])
assert coords == [(1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0)]
def test_linestring(self):
a = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]])
t = a.T
s = geometry.LineString(t)
assert list(s.coords) == [
(1.0, 3.0),
(1.0, 4.0),
(2.0, 4.0),
(2.0, 3.0),
(1.0, 3.0),
]
def test_polygon(self):
a = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]])
t = a.T
s = geometry.Polygon(t)
assert list(s.exterior.coords) == [
(1.0, 3.0),
(1.0, 4.0),
(2.0, 4.0),
(2.0, 3.0),
(1.0, 3.0),
]
@@ -0,0 +1,18 @@
import unittest
import pytest
from shapely.geometry import Point
from shapely.ops import nearest_points
class Nearest(unittest.TestCase):
def test_nearest(self):
first, second = nearest_points(
Point(0, 0).buffer(1.0),
Point(3, 0).buffer(1.0),
)
assert first.x == pytest.approx(1.0)
assert second.x == pytest.approx(2.0)
assert first.y == pytest.approx(0.0)
assert second.y == pytest.approx(0.0)
@@ -0,0 +1,122 @@
import unittest
import pytest
import shapely
from shapely import geos_version
from shapely.errors import TopologicalError
from shapely.geometry import GeometryCollection, LineString, MultiPoint, Point, Polygon
from shapely.wkt import loads
class OperationsTestCase(unittest.TestCase):
def test_operations(self):
point = Point(0.0, 0.0)
# General geometry
assert point.area == 0.0
assert point.length == 0.0
assert point.distance(Point(-1.0, -1.0)) == pytest.approx(1.4142135623730951)
# Topology operations
# Envelope
assert isinstance(point.envelope, Point)
# Intersection
assert point.intersection(Point(-1, -1)).is_empty
# Buffer
assert isinstance(point.buffer(10.0), Polygon)
assert isinstance(point.buffer(10.0, 32), Polygon)
# Simplify
p = loads(
"POLYGON ((120 120, 140 199, 160 200, 180 199, 220 120, 122 122, 121 121, 120 120))"
)
expected = loads(
"POLYGON ((120 120, 140 199, 160 200, 180 199, 220 120, 120 120))"
)
s = p.simplify(10.0, preserve_topology=False)
assert s.equals_exact(expected, 0.001)
p = loads(
"POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200),"
"(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"
)
expected = loads(
"POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200),"
"(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"
)
s = p.simplify(10.0, preserve_topology=True)
assert s.equals_exact(expected, 0.001)
# Convex Hull
assert isinstance(point.convex_hull, Point)
# Differences
assert isinstance(point.difference(Point(-1, 1)), Point)
assert isinstance(point.symmetric_difference(Point(-1, 1)), MultiPoint)
# Boundary
assert isinstance(point.boundary, GeometryCollection)
# Union
assert isinstance(point.union(Point(-1, 1)), MultiPoint)
assert isinstance(point.representative_point(), Point)
assert isinstance(point.point_on_surface(), Point)
assert point.representative_point() == point.point_on_surface()
assert isinstance(point.centroid, Point)
def test_relate(self):
# Relate
assert Point(0, 0).relate(Point(-1, -1)) == "FF0FFF0F2"
# issue #294: should raise TopologicalError on exception
invalid_polygon = loads(
"POLYGON ((40 100, 80 100, 80 60, 40 60, 40 100), (60 60, 80 60, 80 40, 60 40, 60 60))"
)
assert not invalid_polygon.is_valid
if geos_version < (3, 13, 0):
with pytest.raises((TopologicalError, shapely.GEOSException)):
invalid_polygon.relate(invalid_polygon)
else: # resolved with RelateNG
assert invalid_polygon.relate(invalid_polygon) == "2FFF1FFF2"
def test_hausdorff_distance(self):
point = Point(1, 1)
line = LineString([(2, 0), (2, 4), (3, 4)])
distance = point.hausdorff_distance(line)
assert distance == point.distance(Point(3, 4))
def test_interpolate(self):
# successful interpolation
test_line = LineString([(1, 1), (1, 2)])
known_point = Point(1, 1.5)
interpolated_point = test_line.interpolate(0.5, normalized=True)
assert interpolated_point == known_point
# Issue #653; should nog segfault for empty geometries
empty_line = loads("LINESTRING EMPTY")
assert empty_line.is_empty
interpolated_point = empty_line.interpolate(0.5, normalized=True)
assert interpolated_point.is_empty
# invalid geometry should raise TypeError on exception
polygon = loads("POLYGON EMPTY")
with pytest.raises(TypeError, match="incorrect geometry type"):
polygon.interpolate(0.5, normalized=True)
def test_normalize(self):
point = Point(1, 1)
result = point.normalize()
assert result == point
line = loads("MULTILINESTRING ((1 1, 0 0), (1 1, 1 2))")
result = line.normalize()
expected = loads("MULTILINESTRING ((1 1, 1 2), (0 0, 1 1))")
assert result == expected
@@ -0,0 +1,60 @@
import unittest
from shapely.geometry import LineString, MultiPoint, Point, Polygon
class OperatorsTestCase(unittest.TestCase):
def test_point(self):
point = Point(0, 0)
point2 = Point(-1, 1)
assert point.union(point2).equals(point | point2)
assert (point & point2).is_empty
assert point.equals(point - point2)
assert point.symmetric_difference(point2).equals(point ^ point2)
assert point != point2
point_dupe = Point(0, 0)
assert point, point_dupe
def test_multipoint(self):
mp1 = MultiPoint([(0, 0), (1, 1)])
mp1_dup = MultiPoint([(0, 0), (1, 1)])
mp1_rev = MultiPoint([(1, 1), (0, 0)])
mp2 = MultiPoint([(0, 0), (1, 1), (2, 2)])
mp3 = MultiPoint([(0, 0), (1, 1), (2, 3)])
assert mp1 == mp1_dup
assert mp1 != mp1_rev
assert mp1 != mp2
assert mp2 != mp3
p = Point(0, 0)
mp = MultiPoint([(0, 0)])
assert p != mp
assert mp != p
def test_polygon(self):
shell = ((0, 0), (3, 0), (3, 3), (0, 3))
hole = ((1, 1), (2, 1), (2, 2), (1, 2))
p_solid = Polygon(shell)
p2_solid = Polygon(shell)
p_hole = Polygon(shell, holes=[hole])
p2_hole = Polygon(shell, holes=[hole])
assert p_solid == p2_solid
assert p_hole == p2_hole
assert p_solid != p_hole
shell2 = ((-5, 2), (10.5, 3), (7, 3))
p3_hole = Polygon(shell2, holes=[hole])
assert p_hole != p3_hole
def test_linestring(self):
line1 = LineString([(0, 0), (1, 1), (2, 2)])
line2 = LineString([(0, 0), (2, 2)])
line2_dup = LineString([(0, 0), (2, 2)])
# .equals() indicates these are the same
assert line1.equals(line2)
# but != indicates these are different
assert line1 != line2
# but dupes are the same with ==
assert line2 == line2_dup
@@ -0,0 +1,64 @@
import unittest
from shapely.geometry import (
GeometryCollection,
LinearRing,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
from shapely.ops import orient
class OrientTestCase(unittest.TestCase):
def test_point(self):
point = Point(0, 0)
assert orient(point, 1) == point
assert orient(point, -1) == point
def test_multipoint(self):
multipoint = MultiPoint([(0, 0), (1, 1)])
assert orient(multipoint, 1) == multipoint
assert orient(multipoint, -1) == multipoint
def test_linestring(self):
linestring = LineString([(0, 0), (1, 1)])
assert orient(linestring, 1) == linestring
assert orient(linestring, -1) == linestring
def test_multilinestring(self):
multilinestring = MultiLineString([[(0, 0), (1, 1)], [(1, 0), (0, 1)]])
assert orient(multilinestring, 1) == multilinestring
assert orient(multilinestring, -1) == multilinestring
def test_linearring(self):
linearring = LinearRing([(0, 0), (0, 1), (1, 0)])
assert orient(linearring, 1) == linearring
assert orient(linearring, -1) == linearring
def test_polygon(self):
polygon = Polygon([(0, 0), (0, 1), (1, 0)])
polygon_reversed = Polygon(polygon.exterior.coords[::-1])
assert (orient(polygon, 1)) == polygon_reversed
assert (orient(polygon, -1)) == polygon
def test_multipolygon(self):
polygon1 = Polygon([(0, 0), (0, 1), (1, 0)])
polygon2 = Polygon([(1, 0), (2, 0), (2, 1)])
polygon1_reversed = Polygon(polygon1.exterior.coords[::-1])
polygon2_reversed = Polygon(polygon2.exterior.coords[::-1])
multipolygon = MultiPolygon([polygon1, polygon2])
assert not polygon1.exterior.is_ccw
assert polygon2.exterior.is_ccw
assert orient(multipolygon, 1) == MultiPolygon([polygon1_reversed, polygon2])
assert orient(multipolygon, -1) == MultiPolygon([polygon1, polygon2_reversed])
def test_geometrycollection(self):
polygon = Polygon([(0, 0), (0, 1), (1, 0)])
polygon_reversed = Polygon(polygon.exterior.coords[::-1])
collection = GeometryCollection([polygon])
assert orient(collection, 1) == GeometryCollection([polygon_reversed])
assert orient(collection, -1) == GeometryCollection([polygon])
@@ -0,0 +1,60 @@
import unittest
import pytest
from shapely.geometry import LinearRing, LineString
from shapely.testing import assert_geometries_equal
@pytest.mark.parametrize("distance", [float("nan"), float("inf")])
def test_non_finite_distance(distance):
g = LineString([(0, 0), (10, 0)])
with pytest.raises(ValueError, match="distance must be finite"):
g.parallel_offset(distance)
class OperationsTestCase(unittest.TestCase):
def test_parallel_offset_linestring(self):
line1 = LineString([(0, 0), (10, 0)])
left = line1.parallel_offset(5, "left")
assert_geometries_equal(left, LineString([(0, 5), (10, 5)]))
right = line1.parallel_offset(5, "right")
assert_geometries_equal(right, LineString([(10, -5), (0, -5)]), normalize=True)
right = line1.parallel_offset(-5, "left")
assert_geometries_equal(right, LineString([(10, -5), (0, -5)]), normalize=True)
left = line1.parallel_offset(-5, "right")
assert_geometries_equal(left, LineString([(0, 5), (10, 5)]))
# by default, parallel_offset is right-handed
assert_geometries_equal(line1.parallel_offset(5), right)
line2 = LineString([(0, 0), (5, 0), (5, -5)])
assert_geometries_equal(
line2.parallel_offset(2, "left", join_style=3),
LineString([(0, 2), (5, 2), (7, 0), (7, -5)]),
)
assert_geometries_equal(
line2.parallel_offset(2, "left", join_style=2),
LineString([(0, 2), (7, 2), (7, -5)]),
)
# offset_curve alias
assert_geometries_equal(
line1.offset_curve(2, quad_segs=10),
line1.parallel_offset(2, "left", resolution=10),
)
assert_geometries_equal(
line1.offset_curve(-2, join_style="mitre"),
line1.parallel_offset(2, "right", join_style=2),
)
def test_parallel_offset_linear_ring(self):
lr1 = LinearRing([(0, 0), (5, 0), (5, 5), (0, 5), (0, 0)])
assert_geometries_equal(
lr1.parallel_offset(2, "left", resolution=1),
LineString([(2, 2), (3, 2), (3, 3), (2, 3), (2, 2)]),
)
# offset_curve alias
assert_geometries_equal(
lr1.offset_curve(2, quad_segs=1),
lr1.parallel_offset(2, "left", resolution=1),
)
@@ -0,0 +1,51 @@
"""Persistence tests
"""
import pickle
import struct
import unittest
from shapely import wkb, wkt
from shapely.geometry import Point
class PersistTestCase(unittest.TestCase):
def test_pickle(self):
p = Point(0.0, 0.0)
data = pickle.dumps(p)
q = pickle.loads(data)
assert q.equals(p)
def test_wkb(self):
p = Point(0.0, 0.0)
wkb_big_endian = wkb.dumps(p, big_endian=True)
wkb_little_endian = wkb.dumps(p, big_endian=False)
# Regardless of byte order, loads ought to correctly recover the
# geometry
assert p.equals(wkb.loads(wkb_big_endian))
assert p.equals(wkb.loads(wkb_little_endian))
def test_wkb_dumps_endianness(self):
p = Point(0.5, 2.0)
wkb_big_endian = wkb.dumps(p, big_endian=True)
wkb_little_endian = wkb.dumps(p, big_endian=False)
assert wkb_big_endian != wkb_little_endian
# According to WKB specification in section 3.3 of OpenGIS
# Simple Features Specification for SQL, revision 1.1, the
# first byte of a WKB representation indicates byte order.
# Big-endian is 0, little-endian is 1.
assert wkb_big_endian[0] == 0
assert wkb_little_endian[0] == 1
# Check that the doubles (0.5, 2.0) are in correct byte order
double_size = struct.calcsize("d")
assert wkb_big_endian[(-2 * double_size) :] == struct.pack(">2d", p.x, p.y)
assert wkb_little_endian[(-2 * double_size) :] == struct.pack("<2d", p.x, p.y)
def test_wkt(self):
p = Point(0.0, 0.0)
text = wkt.dumps(p)
assert text.startswith("POINT")
pt = wkt.loads(text)
assert pt.equals(p)
@@ -0,0 +1,84 @@
import pathlib
import pickle
import warnings
from pickle import dumps, HIGHEST_PROTOCOL, loads
import pytest
import shapely
from shapely import wkt
from shapely.geometry import (
box,
GeometryCollection,
LinearRing,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
HERE = pathlib.Path(__file__).parent
TEST_DATA = {
"point2d": Point([(1.0, 2.0)]),
"point3d": Point([(1.0, 2.0, 3.0)]),
"linestring": LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]),
"linearring": LinearRing([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]),
"polygon": Polygon([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]),
"multipoint": MultiPoint([(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]),
"multilinestring": MultiLineString(
[[(0.0, 0.0), (1.0, 1.0)], [(1.0, 2.0), (3.0, 3.0)]]
),
"multipolygon": MultiPolygon([box(0, 0, 1, 1), box(2, 2, 3, 3)]),
"geometrycollection": GeometryCollection([Point(1.0, 2.0), box(0, 0, 1, 1)]),
"emptypoint": wkt.loads("POINT EMPTY"),
"emptypolygon": wkt.loads("POLYGON EMPTY"),
}
TEST_NAMES, TEST_GEOMS = zip(*TEST_DATA.items())
@pytest.mark.parametrize("geom1", TEST_GEOMS, ids=TEST_NAMES)
def test_pickle_round_trip(geom1):
data = dumps(geom1, HIGHEST_PROTOCOL)
with warnings.catch_warnings():
warnings.simplefilter("error")
geom2 = loads(data)
assert geom2.has_z == geom1.has_z
assert type(geom2) is type(geom1)
assert geom2.geom_type == geom1.geom_type
assert geom2.wkt == geom1.wkt
@pytest.mark.parametrize(
"fname", (HERE / "data").glob("*.pickle"), ids=lambda fname: fname.name
)
def test_unpickle_pre_20(fname):
from shapely.testing import assert_geometries_equal
geom_type = fname.name.split("_")[0]
expected = TEST_DATA[geom_type]
with open(fname, "rb") as f:
with pytest.warns(UserWarning):
result = pickle.load(f)
assert_geometries_equal(result, expected)
if __name__ == "__main__":
datadir = HERE / "data"
datadir.mkdir(exist_ok=True)
shapely_version = shapely.__version__
print(shapely_version)
print(shapely.geos.geos_version)
for name, geom in TEST_DATA.items():
if name == "emptypoint" and shapely.geos.geos_version < (3, 9, 0):
# Empty Points cannot be represented in WKB
continue
with open(datadir / f"{name}_{shapely_version}.pickle", "wb") as f:
pickle.dump(geom, f)
@@ -0,0 +1,44 @@
import unittest
from shapely.geometry import LineString, Point, Polygon
from shapely.geometry.base import dump_coords
from shapely.ops import polygonize, polygonize_full
class PolygonizeTestCase(unittest.TestCase):
def test_polygonize(self):
lines = [
LineString([(0, 0), (1, 1)]),
LineString([(0, 0), (0, 1)]),
LineString([(0, 1), (1, 1)]),
LineString([(1, 1), (1, 0)]),
LineString([(1, 0), (0, 0)]),
LineString([(5, 5), (6, 6)]),
Point(0, 0),
]
result = list(polygonize(lines))
assert all(isinstance(x, Polygon) for x in result)
def test_polygonize_full(self):
lines2 = [
[(0, 0), (1, 1)],
[(0, 0), (0, 1)],
[(0, 1), (1, 1)],
[(1, 1), (1, 0)],
[(1, 0), (0, 0)],
[(5, 5), (6, 6)],
[(1, 1), (100, 100)],
]
result2, cuts, dangles, invalids = polygonize_full(lines2)
assert len(result2.geoms) == 2
assert all(isinstance(x, Polygon) for x in result2.geoms)
assert list(cuts.geoms) == []
assert all(isinstance(x, LineString) for x in dangles.geoms)
assert dump_coords(dangles) == [
[(1.0, 1.0), (100.0, 100.0)],
[(5.0, 5.0), (6.0, 6.0)],
]
assert list(invalids.geoms) == []
@@ -0,0 +1,97 @@
import unittest
import pytest
from shapely.algorithms.polylabel import Cell, polylabel
from shapely.errors import TopologicalError
from shapely.geometry import LineString, Point, Polygon
class PolylabelTestCase(unittest.TestCase):
def test_polylabel(self):
"""
Finds pole of inaccessibility for a polygon with a tolerance of 10
"""
polygon = LineString(
[(0, 0), (50, 200), (100, 100), (20, 50), (-100, -20), (-150, -200)]
).buffer(100)
label = polylabel(polygon, tolerance=10)
expected = Point(59.35615556364569, 121.8391962974644)
assert expected.equals_exact(label, 1e-6)
def test_invalid_polygon(self):
"""
Makes sure that the polylabel function throws an exception when provided
an invalid polygon.
"""
bowtie_polygon = Polygon(
[(0, 0), (0, 20), (10, 10), (20, 20), (20, 0), (10, 10), (0, 0)]
)
with pytest.raises(TopologicalError):
polylabel(bowtie_polygon)
def test_cell_sorting(self):
"""
Tests rich comparison operators of Cells for use in the polylabel
minimum priority queue.
"""
polygon = Point(0, 0).buffer(100)
cell1 = Cell(0, 0, 50, polygon) # closest
cell2 = Cell(50, 50, 50, polygon) # furthest
assert cell1 < cell2
assert cell1 <= cell2
assert (cell2 <= cell1) is False
assert cell1 == cell1
assert (cell1 == cell2) is False
assert cell1 != cell2
assert (cell1 != cell1) is False
assert cell2 > cell1
assert (cell1 > cell2) is False
assert cell2 >= cell1
assert (cell1 >= cell2) is False
def test_concave_polygon(self):
"""
Finds pole of inaccessibility for a concave polygon and ensures that
the point is inside.
"""
concave_polygon = LineString([(500, 0), (0, 0), (0, 500), (500, 500)]).buffer(
100
)
label = polylabel(concave_polygon)
assert concave_polygon.contains(label)
def test_rectangle_special_case(self):
"""
The centroid algorithm used is vulnerable to floating point errors
and can give unexpected results for rectangular polygons. Test
that this special case is handled correctly.
https://github.com/mapbox/polylabel/issues/3
"""
polygon = Polygon(
[
(32.71997, -117.19310),
(32.71997, -117.21065),
(32.72408, -117.21065),
(32.72408, -117.19310),
]
)
label = polylabel(polygon)
assert label.coords[:] == [(32.722025, -117.201875)]
def test_polygon_with_hole(self):
"""
Finds pole of inaccessibility for a polygon with a hole
https://github.com/shapely/shapely/issues/817
"""
polygon = Polygon(
shell=[(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
holes=[[(2, 2), (6, 2), (6, 6), (2, 6), (2, 2)]],
)
label = polylabel(polygon, 0.05)
assert label.x == pytest.approx(7.65625)
assert label.y == pytest.approx(7.65625)
@@ -0,0 +1,99 @@
"""Test GEOS predicates
"""
import unittest
import pytest
import shapely
from shapely import geos_version
from shapely.geometry import Point, Polygon
class PredicatesTestCase(unittest.TestCase):
def test_binary_predicates(self):
point = Point(0.0, 0.0)
point2 = Point(2.0, 2.0)
assert point.disjoint(Point(-1.0, -1.0))
assert not point.touches(Point(-1.0, -1.0))
assert not point.crosses(Point(-1.0, -1.0))
assert not point.within(Point(-1.0, -1.0))
assert not point.contains(Point(-1.0, -1.0))
assert not point.equals(Point(-1.0, -1.0))
assert not point.touches(Point(-1.0, -1.0))
assert point.equals(Point(0.0, 0.0))
assert point.covers(Point(0.0, 0.0))
assert point.covered_by(Point(0.0, 0.0))
assert not point.covered_by(point2)
assert not point2.covered_by(point)
assert not point.covers(Point(-1.0, -1.0))
def test_unary_predicates(self):
point = Point(0.0, 0.0)
assert not point.is_empty
assert point.is_valid
assert point.is_simple
assert not point.is_ring
assert not point.has_z
def test_binary_predicate_exceptions(self):
p1 = [
(339, 346),
(459, 346),
(399, 311),
(340, 277),
(399, 173),
(280, 242),
(339, 415),
(280, 381),
(460, 207),
(339, 346),
]
p2 = [
(339, 207),
(280, 311),
(460, 138),
(399, 242),
(459, 277),
(459, 415),
(399, 381),
(519, 311),
(520, 242),
(519, 173),
(399, 450),
(339, 207),
]
g1 = Polygon(p1)
g2 = Polygon(p2)
assert not g1.is_valid
assert not g2.is_valid
if geos_version < (3, 13, 0):
with pytest.raises(shapely.GEOSException):
g1.within(g2)
else: # resolved with RelateNG
assert not g1.within(g2)
def test_relate_pattern(self):
# a pair of partially overlapping polygons, and a nearby point
g1 = Polygon([(0, 0), (0, 1), (3, 1), (3, 0), (0, 0)])
g2 = Polygon([(1, -1), (1, 2), (2, 2), (2, -1), (1, -1)])
g3 = Point(5, 5)
assert g1.relate(g2) == "212101212"
assert g1.relate_pattern(g2, "212101212")
assert g1.relate_pattern(g2, "*********")
assert g1.relate_pattern(g2, "2********")
assert g1.relate_pattern(g2, "T********")
assert not g1.relate_pattern(g2, "112101212")
assert not g1.relate_pattern(g2, "1********")
assert g1.relate_pattern(g3, "FF2FF10F2")
# an invalid pattern should raise an exception
with pytest.raises(shapely.GEOSException, match="IllegalArgumentException"):
g1.relate_pattern(g2, "fail")
@@ -0,0 +1,62 @@
import numpy as np
import pytest
from shapely.geometry import Point, Polygon
from shapely.prepared import prep, PreparedGeometry
def test_prepared_geometry():
polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
p = PreparedGeometry(polygon)
assert p.contains(Point(0.5, 0.5))
assert not p.contains(Point(0.5, 1.5))
def test_prep():
polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
p = prep(polygon)
assert p.contains(Point(0.5, 0.5))
assert not p.contains(Point(0.5, 1.5))
def test_op_not_allowed():
p = PreparedGeometry(Point(0.0, 0.0).buffer(1.0))
with pytest.raises(TypeError):
Point(0.0, 0.0).union(p)
def test_predicate_not_allowed():
p = PreparedGeometry(Point(0.0, 0.0).buffer(1.0))
with pytest.raises(TypeError):
Point(0.0, 0.0).contains(p)
def test_prepared_predicates():
# check prepared predicates give the same result as regular predicates
polygon1 = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
polygon2 = Polygon([(0.5, 0.5), (1.5, 0.5), (1.0, 1.0), (0.5, 0.5)])
point2 = Point(0.5, 0.5)
polygon_empty = Polygon()
prepared_polygon1 = PreparedGeometry(polygon1)
for geom2 in (polygon2, point2, polygon_empty):
with np.errstate(invalid="ignore"):
assert polygon1.disjoint(geom2) == prepared_polygon1.disjoint(geom2)
assert polygon1.touches(geom2) == prepared_polygon1.touches(geom2)
assert polygon1.intersects(geom2) == prepared_polygon1.intersects(geom2)
assert polygon1.crosses(geom2) == prepared_polygon1.crosses(geom2)
assert polygon1.within(geom2) == prepared_polygon1.within(geom2)
assert polygon1.contains(geom2) == prepared_polygon1.contains(geom2)
assert polygon1.overlaps(geom2) == prepared_polygon1.overlaps(geom2)
def test_prepare_already_prepared():
polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
prepared = prep(polygon)
# attempt to prepare an already prepared geometry with `prep`
result = prep(prepared)
assert isinstance(result, PreparedGeometry)
assert result.context is polygon
# attempt to prepare an already prepared geometry with `PreparedGeometry`
result = PreparedGeometry(prepared)
assert isinstance(result, PreparedGeometry)
assert result.context is polygon
@@ -0,0 +1,13 @@
import unittest
from shapely.geometry import LineString
class ProductZTestCase(unittest.TestCase):
def test_line_intersection(self):
line1 = LineString([(0, 0, 0), (1, 1, 1)])
line2 = LineString([(0, 1, 1), (1, 0, 0)])
interxn = line1.intersection(line2)
assert interxn.has_z
assert interxn._ndim == 3
assert 0.0 <= interxn.z <= 1.0
@@ -0,0 +1,48 @@
import pytest
from shapely.geometry import MultiLineString, Polygon, shape
from shapely.geometry.geo import _is_coordinates_empty
@pytest.mark.parametrize(
"geom",
[{"type": "Polygon", "coordinates": None}, {"type": "Polygon", "coordinates": []}],
)
def test_polygon_no_coords(geom):
assert shape(geom) == Polygon()
def test_polygon_empty_np_array():
np = pytest.importorskip("numpy")
geom = {"type": "Polygon", "coordinates": np.array([])}
assert shape(geom) == Polygon()
def test_polygon_with_coords_list():
geom = {"type": "Polygon", "coordinates": [[[5, 10], [10, 10], [10, 5]]]}
obj = shape(geom)
assert obj == Polygon([(5, 10), (10, 10), (10, 5)])
def test_polygon_not_empty_np_array():
np = pytest.importorskip("numpy")
geom = {"type": "Polygon", "coordinates": np.array([[[5, 10], [10, 10], [10, 5]]])}
obj = shape(geom)
assert obj == Polygon([(5, 10), (10, 10), (10, 5)])
@pytest.mark.parametrize(
"geom",
[
{"type": "MultiLineString", "coordinates": []},
{"type": "MultiLineString", "coordinates": [[]]},
{"type": "MultiLineString", "coordinates": None},
],
)
def test_multilinestring_empty(geom):
assert shape(geom) == MultiLineString()
@pytest.mark.parametrize("coords", [[], [[]], [[], []], None, [[[]]]])
def test_is_coordinates_empty(coords):
assert _is_coordinates_empty(coords)
@@ -0,0 +1,45 @@
import unittest
import pytest
from shapely.errors import GeometryTypeError
from shapely.geometry import GeometryCollection, LineString, MultiLineString, Point
from shapely.ops import shared_paths
class SharedPaths(unittest.TestCase):
def test_shared_paths_forward(self):
g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
g2 = LineString([(5, 0), (15, 0)])
result = shared_paths(g1, g2)
assert isinstance(result, GeometryCollection)
assert len(result.geoms) == 2
a, b = result.geoms
assert isinstance(a, MultiLineString)
assert len(a.geoms) == 1
assert a.geoms[0].coords[:] == [(5, 0), (10, 0)]
assert b.is_empty
def test_shared_paths_forward2(self):
g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
g2 = LineString([(15, 0), (5, 0)])
result = shared_paths(g1, g2)
assert isinstance(result, GeometryCollection)
assert len(result.geoms) == 2
a, b = result.geoms
assert isinstance(b, MultiLineString)
assert len(b.geoms) == 1
assert b.geoms[0].coords[:] == [(5, 0), (10, 0)]
assert a.is_empty
def test_wrong_type(self):
g1 = Point(0, 0)
g2 = LineString([(5, 0), (15, 0)])
with pytest.raises(GeometryTypeError):
shared_paths(g1, g2)
with pytest.raises(GeometryTypeError):
shared_paths(g2, g1)
@@ -0,0 +1,15 @@
import unittest
from shapely.geometry import Polygon
class PolygonTestCase(unittest.TestCase):
def test_polygon_3(self):
p = (1.0, 1.0)
poly = Polygon([p, p, p])
assert poly.bounds == (1.0, 1.0, 1.0, 1.0)
def test_polygon_5(self):
p = (1.0, 1.0)
poly = Polygon([p, p, p, p, p])
assert poly.bounds == (1.0, 1.0, 1.0, 1.0)
@@ -0,0 +1,25 @@
import unittest
from shapely.geometry import LineString, Polygon
from shapely.ops import snap
class Snap(unittest.TestCase):
def test_snap(self):
# input geometries
square = Polygon([(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)])
line = LineString([(0, 0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)])
square_coords = square.exterior.coords[:]
line_coords = line.coords[:]
result = snap(line, square, 0.5)
# test result is correct
assert isinstance(result, LineString)
assert result.coords[:] == [(0.0, 0.0), (1.0, 1.0), (2.0, 1.0), (2.6, 0.5)]
# test inputs have not been modified
assert square.exterior.coords[:] == square_coords
assert line.coords[:] == line_coords

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