721 lines
25 KiB
Python
721 lines
25 KiB
Python
"""Support for various GEOS geometry operations."""
|
|
|
|
import shapely
|
|
from shapely.algorithms.polylabel import polylabel # noqa
|
|
from shapely.errors import GeometryTypeError
|
|
from shapely.geometry import (
|
|
GeometryCollection,
|
|
LineString,
|
|
MultiLineString,
|
|
MultiPoint,
|
|
Point,
|
|
Polygon,
|
|
shape,
|
|
)
|
|
from shapely.geometry.base import BaseGeometry
|
|
from shapely.prepared import prep
|
|
|
|
__all__ = [
|
|
"clip_by_rect",
|
|
"linemerge",
|
|
"nearest_points",
|
|
"operator",
|
|
"orient",
|
|
"polygonize",
|
|
"polygonize_full",
|
|
"shared_paths",
|
|
"snap",
|
|
"split",
|
|
"substring",
|
|
"transform",
|
|
"triangulate",
|
|
"unary_union",
|
|
"validate",
|
|
"voronoi_diagram",
|
|
]
|
|
|
|
|
|
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):
|
|
"""Create 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):
|
|
"""Create polygons from a source of lines.
|
|
|
|
The polygons and leftover geometries are returned as well.
|
|
|
|
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):
|
|
"""Merge 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 unary_union(self, geoms):
|
|
"""Return 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
|
|
unary_union = operator.unary_union
|
|
|
|
|
|
def triangulate(geom, tolerance=0.0, edges=False):
|
|
"""Create the Delaunay triangulation and return 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 list(collection.geoms)
|
|
|
|
|
|
def voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False):
|
|
"""Construct 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 True if the geometry is valid."""
|
|
return shapely.is_valid_reason(geom)
|
|
|
|
|
|
def transform(func, geom):
|
|
"""Apply `func` to all coordinates of `geom`.
|
|
|
|
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 = [
|
|
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 = [
|
|
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):
|
|
"""Return 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, MultiLineString)):
|
|
raise GeometryTypeError("Second argument must be a (Multi)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 # noqa: E501
|
|
# | Boundary | 0 or F | * | * | So either '0********' or '[0F]**0*****' # noqa: E501
|
|
# | Exterior | * | * | * | No overlapping interiors ('1********') # noqa: E501
|
|
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):
|
|
"""Split a geometry by another geometry and return 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
|
|
|
|
Examples
|
|
--------
|
|
>>> import shapely.ops
|
|
>>> from shapely import Point, LineString
|
|
>>> pt = Point((1, 1))
|
|
>>> line = LineString([(0,0), (2,2)])
|
|
>>> result = shapely.ops.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 in ("LineString", "MultiLineString"):
|
|
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=normalized)
|
|
elif not normalized and start_dist >= geom.length and end_dist >= geom.length:
|
|
return geom.interpolate(geom.length, normalized=normalized)
|
|
elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length:
|
|
return geom.interpolate(0, normalized=normalized)
|
|
elif normalized and start_dist >= 1 and end_dist >= 1:
|
|
return geom.interpolate(1, normalized=normalized)
|
|
elif normalized and -start_dist >= 1 and -end_dist >= 1:
|
|
return geom.interpolate(0, normalized=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
|
|
|
|
start_dist = max(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:]): # noqa
|
|
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):
|
|
"""Return 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
|
|
-----
|
|
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):
|
|
"""Return 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.
|
|
|
|
It is recommended to use :func:`shapely.orient_polygons` instead.
|
|
|
|
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
|
|
|
|
"""
|
|
return shapely.orient_polygons(geom, exterior_cw=sign < 0)
|