linestring.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. """Line strings and related utilities."""
  2. import numpy as np
  3. import shapely
  4. from shapely.decorators import deprecate_positional
  5. from shapely.geometry.base import JOIN_STYLE, BaseGeometry
  6. from shapely.geometry.point import Point
  7. __all__ = ["LineString"]
  8. class LineString(BaseGeometry):
  9. """A geometry type composed of one or more line segments.
  10. A LineString is a one-dimensional feature and has a non-zero length but
  11. zero area. It may approximate a curve and need not be straight. A LineString may
  12. be closed.
  13. Parameters
  14. ----------
  15. coordinates : sequence
  16. A sequence of (x, y, [,z]) numeric coordinate pairs or triples, or
  17. an array-like with shape (N, 2) or (N, 3).
  18. Also can be a sequence of Point objects, or combination of both.
  19. Examples
  20. --------
  21. Create a LineString with two segments
  22. >>> from shapely import LineString
  23. >>> a = LineString([[0, 0], [1, 0], [1, 1]])
  24. >>> a.length
  25. 2.0
  26. """
  27. __slots__ = []
  28. def __new__(self, coordinates=None):
  29. """Create a new LineString geometry."""
  30. if coordinates is None:
  31. # empty geometry
  32. # TODO better constructor
  33. return shapely.from_wkt("LINESTRING EMPTY")
  34. elif isinstance(coordinates, LineString):
  35. if type(coordinates) is LineString:
  36. # return original objects since geometries are immutable
  37. return coordinates
  38. else:
  39. # LinearRing
  40. # TODO convert LinearRing to LineString more directly
  41. coordinates = coordinates.coords
  42. else:
  43. if hasattr(coordinates, "__array__"):
  44. coordinates = np.asarray(coordinates)
  45. if isinstance(coordinates, np.ndarray) and np.issubdtype(
  46. coordinates.dtype, np.number
  47. ):
  48. pass
  49. else:
  50. # check coordinates on points
  51. def _coords(o):
  52. if isinstance(o, Point):
  53. return o.coords[0]
  54. else:
  55. return [float(c) for c in o]
  56. coordinates = [_coords(o) for o in coordinates]
  57. if len(coordinates) == 0:
  58. # empty geometry
  59. # TODO better constructor + should shapely.linestrings handle this?
  60. return shapely.from_wkt("LINESTRING EMPTY")
  61. geom = shapely.linestrings(coordinates)
  62. if not isinstance(geom, LineString):
  63. raise ValueError("Invalid values passed to LineString constructor")
  64. return geom
  65. @property
  66. def __geo_interface__(self):
  67. """Return a GeoJSON-like mapping of the LineString geometry."""
  68. return {"type": "LineString", "coordinates": tuple(self.coords)}
  69. def svg(self, scale_factor=1.0, stroke_color=None, opacity=None):
  70. """Return SVG polyline element for the LineString geometry.
  71. Parameters
  72. ----------
  73. scale_factor : float
  74. Multiplication factor for the SVG stroke-width. Default is 1.
  75. stroke_color : str, optional
  76. Hex string for stroke color. Default is to use "#66cc99" if
  77. geometry is valid, and "#ff3333" if invalid.
  78. opacity : float
  79. Float number between 0 and 1 for color opacity. Default value is 0.8
  80. """
  81. if self.is_empty:
  82. return "<g />"
  83. if stroke_color is None:
  84. stroke_color = "#66cc99" if self.is_valid else "#ff3333"
  85. if opacity is None:
  86. opacity = 0.8
  87. pnt_format = " ".join(["{},{}".format(*c) for c in self.coords])
  88. return (
  89. f'<polyline fill="none" stroke="{stroke_color}" '
  90. f'stroke-width="{2.0 * scale_factor}" '
  91. f'points="{pnt_format}" opacity="{opacity}" />'
  92. )
  93. @property
  94. def xy(self):
  95. """Separate arrays of X and Y coordinate values.
  96. Examples
  97. --------
  98. >>> from shapely import LineString
  99. >>> x, y = LineString([(0, 0), (1, 1)]).xy
  100. >>> list(x)
  101. [0.0, 1.0]
  102. >>> list(y)
  103. [0.0, 1.0]
  104. """
  105. return self.coords.xy
  106. # Note: future plan is to change this signature over a few releases:
  107. # shapely 2.0:
  108. # offset_curve(self, distance, quad_segs=16, ...)
  109. # shapely 2.1: shows deprecation warning about positional 'quad_segs', etc.
  110. # same signature as 2.0
  111. # shapely 2.2(?): enforce keyword-only arguments after 'distance'
  112. # offset_curve(self, distance, *, quad_segs=16, ...)
  113. @deprecate_positional(
  114. ["quad_segs", "join_style", "mitre_limit"], category=DeprecationWarning
  115. )
  116. def offset_curve(
  117. self,
  118. distance,
  119. quad_segs=16,
  120. join_style=JOIN_STYLE.round,
  121. mitre_limit=5.0,
  122. ):
  123. """Return a (Multi)LineString at a distance from the object.
  124. The side, left or right, is determined by the sign of the `distance`
  125. parameter (negative for right side offset, positive for left side
  126. offset). The resolution of the buffer around each vertex of the object
  127. increases by increasing the `quad_segs` keyword parameter.
  128. The join style is for outside corners between line segments. Accepted
  129. values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and
  130. JOIN_STYLE.bevel (3).
  131. The mitre ratio limit is used for very sharp corners. It is the ratio
  132. of the distance from the corner to the end of the mitred offset corner.
  133. When two line segments meet at a sharp angle, a miter join will extend
  134. far beyond the original geometry. To prevent unreasonable geometry, the
  135. mitre limit allows controlling the maximum length of the join corner.
  136. Corners with a ratio which exceed the limit will be beveled.
  137. Note: the behaviour regarding orientation of the resulting line
  138. depends on the GEOS version. With GEOS < 3.11, the line retains the
  139. same direction for a left offset (positive distance) or has reverse
  140. direction for a right offset (negative distance), and this behaviour
  141. was documented as such in previous Shapely versions. Starting with
  142. GEOS 3.11, the function tries to preserve the orientation of the
  143. original line.
  144. """
  145. if mitre_limit == 0.0:
  146. raise ValueError("Cannot compute offset from zero-length line segment")
  147. elif not np.isfinite(distance):
  148. raise ValueError("offset_curve distance must be finite")
  149. return shapely.offset_curve(
  150. self,
  151. distance,
  152. quad_segs=quad_segs,
  153. join_style=join_style,
  154. mitre_limit=mitre_limit,
  155. )
  156. def parallel_offset(
  157. self,
  158. distance,
  159. side="right",
  160. resolution=16,
  161. join_style=JOIN_STYLE.round,
  162. mitre_limit=5.0,
  163. ):
  164. """Alternative method to :meth:`offset_curve` method.
  165. Older alternative method to the :meth:`offset_curve` method, but uses
  166. ``resolution`` instead of ``quad_segs`` and a ``side`` keyword
  167. ('left' or 'right') instead of sign of the distance. This method is
  168. kept for backwards compatibility for now, but is is recommended to
  169. use :meth:`offset_curve` instead.
  170. """
  171. if side == "right":
  172. distance *= -1
  173. return self.offset_curve(
  174. distance,
  175. quad_segs=resolution,
  176. join_style=join_style,
  177. mitre_limit=mitre_limit,
  178. )
  179. shapely.lib.registry[1] = LineString