| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- """Polygons and their linear ring components."""
- import numpy as np
- import shapely
- from shapely import _geometry_helpers
- from shapely.algorithms.cga import signed_area # noqa
- from shapely.errors import TopologicalError
- from shapely.geometry.base import BaseGeometry
- from shapely.geometry.linestring import LineString
- from shapely.geometry.point import Point
- __all__ = ["LinearRing", "Polygon", "orient"]
- def _unpickle_linearring(wkb):
- linestring = shapely.from_wkb(wkb)
- srid = shapely.get_srid(linestring)
- linearring = _geometry_helpers.linestring_to_linearring(linestring)
- if srid:
- linearring = shapely.set_srid(linearring, srid)
- return linearring
- class LinearRing(LineString):
- """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.
- >>> from shapely import LinearRing
- >>> 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):
- """Create a new LinearRing geometry."""
- if coordinates is None:
- # empty geometry
- # TODO better way?
- return shapely.from_wkt("LINEARRING EMPTY")
- elif isinstance(coordinates, LineString):
- if type(coordinates) is 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 a GeoJSON-like mapping of the LinearRing geometry."""
- return {"type": "LinearRing", "coordinates": tuple(self.coords)}
- def __reduce__(self):
- """Pickle support.
- 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 if the ring is oriented counter clock-wise."""
- return bool(shapely.is_ccw(self))
- @property
- def is_simple(self):
- """True if the geometry is simple.
- Simple means that any self-intersections are only at boundary points.
- """
- 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
- >>> from shapely import Polygon
- >>> 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):
- """Create a new Polygon geometry."""
- 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 the exterior ring of the polygon."""
- return shapely.get_exterior_ring(self)
- @property
- def interiors(self):
- """Return the sequence of interior rings of the polygon."""
- if self.is_empty:
- return []
- return InteriorRingSequence(self)
- @property
- def coords(self):
- """Not implemented for polygons."""
- raise NotImplementedError(
- "Component rings have coordinate sequences, but the polygon does not"
- )
- @property
- def __geo_interface__(self):
- """Return a GeoJSON-like mapping of the Polygon geometry."""
- 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):
- """Return 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 (
- f'<path fill-rule="evenodd" fill="{fill_color}" stroke="#555555" '
- f'stroke-width="{2.0 * scale_factor}" opacity="{opacity}" d="{path}" />'
- )
- @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):
- """Return an oriented polygon.
- It is recommended to use :func:`shapely.orient_polygons` instead.
- Parameters
- ----------
- polygon : shapely.Polygon
- sign : float, default 1.
- The sign of the result's signed area.
- A non-negative sign means that the coordinates of the geometry's exterior
- rings will be oriented counter-clockwise.
- Returns
- -------
- Geometry or array_like
- Refer to :func:`shapely.orient_polygons` for full documentation.
- """
- return shapely.orient_polygons(polygon, exterior_cw=sign < 0.0)
|