polygon.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. """Polygons and their linear ring components."""
  2. import numpy as np
  3. import shapely
  4. from shapely import _geometry_helpers
  5. from shapely.algorithms.cga import signed_area # noqa
  6. from shapely.errors import TopologicalError
  7. from shapely.geometry.base import BaseGeometry
  8. from shapely.geometry.linestring import LineString
  9. from shapely.geometry.point import Point
  10. __all__ = ["LinearRing", "Polygon", "orient"]
  11. def _unpickle_linearring(wkb):
  12. linestring = shapely.from_wkb(wkb)
  13. srid = shapely.get_srid(linestring)
  14. linearring = _geometry_helpers.linestring_to_linearring(linestring)
  15. if srid:
  16. linearring = shapely.set_srid(linearring, srid)
  17. return linearring
  18. class LinearRing(LineString):
  19. """Geometry type composed of one or more line segments that forms a closed loop.
  20. A LinearRing is a closed, one-dimensional feature.
  21. A LinearRing that crosses itself or touches itself at a single point is
  22. invalid and operations on it may fail.
  23. Parameters
  24. ----------
  25. coordinates : sequence
  26. A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
  27. an array-like with shape (N, 2) or (N, 3).
  28. Also can be a sequence of Point objects.
  29. Notes
  30. -----
  31. Rings are automatically closed. There is no need to specify a final
  32. coordinate pair identical to the first.
  33. Examples
  34. --------
  35. Construct a square ring.
  36. >>> from shapely import LinearRing
  37. >>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) )
  38. >>> ring.is_closed
  39. True
  40. >>> list(ring.coords)
  41. [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
  42. >>> ring.length
  43. 4.0
  44. """
  45. __slots__ = []
  46. def __new__(self, coordinates=None):
  47. """Create a new LinearRing geometry."""
  48. if coordinates is None:
  49. # empty geometry
  50. # TODO better way?
  51. return shapely.from_wkt("LINEARRING EMPTY")
  52. elif isinstance(coordinates, LineString):
  53. if type(coordinates) is LinearRing:
  54. # return original objects since geometries are immutable
  55. return coordinates
  56. elif not coordinates.is_valid:
  57. raise TopologicalError("An input LineString must be valid.")
  58. else:
  59. # LineString
  60. # TODO convert LineString to LinearRing more directly?
  61. coordinates = coordinates.coords
  62. else:
  63. if hasattr(coordinates, "__array__"):
  64. coordinates = np.asarray(coordinates)
  65. if isinstance(coordinates, np.ndarray) and np.issubdtype(
  66. coordinates.dtype, np.number
  67. ):
  68. pass
  69. else:
  70. # check coordinates on points
  71. def _coords(o):
  72. if isinstance(o, Point):
  73. return o.coords[0]
  74. else:
  75. return [float(c) for c in o]
  76. coordinates = np.array([_coords(o) for o in coordinates])
  77. if not np.issubdtype(coordinates.dtype, np.number):
  78. # conversion of coords to 2D array failed, this might be due
  79. # to inconsistent coordinate dimensionality
  80. raise ValueError("Inconsistent coordinate dimensionality")
  81. if len(coordinates) == 0:
  82. # empty geometry
  83. # TODO better constructor + should shapely.linearrings handle this?
  84. return shapely.from_wkt("LINEARRING EMPTY")
  85. geom = shapely.linearrings(coordinates)
  86. if not isinstance(geom, LinearRing):
  87. raise ValueError("Invalid values passed to LinearRing constructor")
  88. return geom
  89. @property
  90. def __geo_interface__(self):
  91. """Return a GeoJSON-like mapping of the LinearRing geometry."""
  92. return {"type": "LinearRing", "coordinates": tuple(self.coords)}
  93. def __reduce__(self):
  94. """Pickle support.
  95. WKB doesn't differentiate between LineString and LinearRing so we
  96. need to move the coordinate sequence into the correct geometry type
  97. """
  98. return (_unpickle_linearring, (shapely.to_wkb(self, include_srid=True),))
  99. @property
  100. def is_ccw(self):
  101. """True if the ring is oriented counter clock-wise."""
  102. return bool(shapely.is_ccw(self))
  103. @property
  104. def is_simple(self):
  105. """True if the geometry is simple.
  106. Simple means that any self-intersections are only at boundary points.
  107. """
  108. return bool(shapely.is_simple(self))
  109. shapely.lib.registry[2] = LinearRing
  110. class InteriorRingSequence:
  111. _parent = None
  112. _ndim = None
  113. _index = 0
  114. _length = 0
  115. def __init__(self, parent):
  116. self._parent = parent
  117. self._ndim = parent._ndim
  118. def __iter__(self):
  119. self._index = 0
  120. self._length = self.__len__()
  121. return self
  122. def __next__(self):
  123. if self._index < self._length:
  124. ring = self._get_ring(self._index)
  125. self._index += 1
  126. return ring
  127. else:
  128. raise StopIteration
  129. def __len__(self):
  130. return shapely.get_num_interior_rings(self._parent)
  131. def __getitem__(self, key):
  132. m = self.__len__()
  133. if isinstance(key, int):
  134. if key + m < 0 or key >= m:
  135. raise IndexError("index out of range")
  136. if key < 0:
  137. i = m + key
  138. else:
  139. i = key
  140. return self._get_ring(i)
  141. elif isinstance(key, slice):
  142. res = []
  143. start, stop, stride = key.indices(m)
  144. for i in range(start, stop, stride):
  145. res.append(self._get_ring(i))
  146. return res
  147. else:
  148. raise TypeError("key must be an index or slice")
  149. def _get_ring(self, i):
  150. return shapely.get_interior_ring(self._parent, i)
  151. class Polygon(BaseGeometry):
  152. """A geometry type representing an area that is enclosed by a linear ring.
  153. A polygon is a two-dimensional feature and has a non-zero area. It may
  154. have one or more negative-space "holes" which are also bounded by linear
  155. rings. If any rings cross each other, the feature is invalid and
  156. operations on it may fail.
  157. Parameters
  158. ----------
  159. shell : sequence
  160. A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
  161. an array-like with shape (N, 2) or (N, 3).
  162. Also can be a sequence of Point objects.
  163. holes : sequence
  164. A sequence of objects which satisfy the same requirements as the
  165. shell parameters above
  166. Attributes
  167. ----------
  168. exterior : LinearRing
  169. The ring which bounds the positive space of the polygon.
  170. interiors : sequence
  171. A sequence of rings which bound all existing holes.
  172. Examples
  173. --------
  174. Create a square polygon with no holes
  175. >>> from shapely import Polygon
  176. >>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.))
  177. >>> polygon = Polygon(coords)
  178. >>> polygon.area
  179. 1.0
  180. """
  181. __slots__ = []
  182. def __new__(self, shell=None, holes=None):
  183. """Create a new Polygon geometry."""
  184. if shell is None:
  185. # empty geometry
  186. # TODO better way?
  187. return shapely.from_wkt("POLYGON EMPTY")
  188. elif isinstance(shell, Polygon):
  189. # return original objects since geometries are immutable
  190. return shell
  191. else:
  192. shell = LinearRing(shell)
  193. if holes is not None:
  194. if len(holes) == 0:
  195. # shapely constructor cannot handle holes=[]
  196. holes = None
  197. else:
  198. holes = [LinearRing(ring) for ring in holes]
  199. geom = shapely.polygons(shell, holes=holes)
  200. if not isinstance(geom, Polygon):
  201. raise ValueError("Invalid values passed to Polygon constructor")
  202. return geom
  203. @property
  204. def exterior(self):
  205. """Return the exterior ring of the polygon."""
  206. return shapely.get_exterior_ring(self)
  207. @property
  208. def interiors(self):
  209. """Return the sequence of interior rings of the polygon."""
  210. if self.is_empty:
  211. return []
  212. return InteriorRingSequence(self)
  213. @property
  214. def coords(self):
  215. """Not implemented for polygons."""
  216. raise NotImplementedError(
  217. "Component rings have coordinate sequences, but the polygon does not"
  218. )
  219. @property
  220. def __geo_interface__(self):
  221. """Return a GeoJSON-like mapping of the Polygon geometry."""
  222. if self.exterior == LinearRing():
  223. coords = []
  224. else:
  225. coords = [tuple(self.exterior.coords)]
  226. for hole in self.interiors:
  227. coords.append(tuple(hole.coords))
  228. return {"type": "Polygon", "coordinates": tuple(coords)}
  229. def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
  230. """Return SVG path element for the Polygon geometry.
  231. Parameters
  232. ----------
  233. scale_factor : float
  234. Multiplication factor for the SVG stroke-width. Default is 1.
  235. fill_color : str, optional
  236. Hex string for fill color. Default is to use "#66cc99" if
  237. geometry is valid, and "#ff3333" if invalid.
  238. opacity : float
  239. Float number between 0 and 1 for color opacity. Default value is 0.6
  240. """
  241. if self.is_empty:
  242. return "<g />"
  243. if fill_color is None:
  244. fill_color = "#66cc99" if self.is_valid else "#ff3333"
  245. if opacity is None:
  246. opacity = 0.6
  247. exterior_coords = [["{},{}".format(*c) for c in self.exterior.coords]]
  248. interior_coords = [
  249. ["{},{}".format(*c) for c in interior.coords] for interior in self.interiors
  250. ]
  251. path = " ".join(
  252. [
  253. "M {} L {} z".format(coords[0], " L ".join(coords[1:]))
  254. for coords in exterior_coords + interior_coords
  255. ]
  256. )
  257. return (
  258. f'<path fill-rule="evenodd" fill="{fill_color}" stroke="#555555" '
  259. f'stroke-width="{2.0 * scale_factor}" opacity="{opacity}" d="{path}" />'
  260. )
  261. @classmethod
  262. def from_bounds(cls, xmin, ymin, xmax, ymax):
  263. """Construct a `Polygon()` from spatial bounds."""
  264. return cls([(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
  265. shapely.lib.registry[3] = Polygon
  266. def orient(polygon, sign=1.0):
  267. """Return an oriented polygon.
  268. It is recommended to use :func:`shapely.orient_polygons` instead.
  269. Parameters
  270. ----------
  271. polygon : shapely.Polygon
  272. sign : float, default 1.
  273. The sign of the result's signed area.
  274. A non-negative sign means that the coordinates of the geometry's exterior
  275. rings will be oriented counter-clockwise.
  276. Returns
  277. -------
  278. Geometry or array_like
  279. Refer to :func:`shapely.orient_polygons` for full documentation.
  280. """
  281. return shapely.orient_polygons(polygon, exterior_cw=sign < 0.0)