refactor: excel parse
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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)
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
Binary file not shown.
@@ -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]
|
||||
+78
@@ -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))"
|
||||
)
|
||||
+214
@@ -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"] == ()
|
||||
+28
@@ -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
|
||||
+72
@@ -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
|
||||
+40
@@ -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
Reference in New Issue
Block a user