Source code for shapelib.tools.lines.proximity.core

"""Remove every portion of a line that is closer than a threshold to another line.

Lines exported to machinery cannot pass too close to each other: below a minimum
separation, working side by side on two rows at once becomes an accident risk.
This tool is the safety preprocessing step that guarantees the exported set has
no location where two lines are closer than ``min_distance`` — the too-close
segments are removed, lines entirely too close disappear, and lines crossing the
forbidden corridor of another are broken into separate pieces.

The public entry point is :func:`remove_close_segments`.
"""

import geopandas as gpd
import shapely
from shapely.geometry import LineString


[docs] def remove_close_segments( lines: gpd.GeoDataFrame, min_distance: float, min_length: float = 0.0, ) -> gpd.GeoDataFrame: """ Remove every portion of a line that is closer than ``min_distance`` to another line. Lines are processed from the longest to the shortest. Each line is compared against the ones already kept: the portions closer than ``min_distance`` to a kept line are removed and the remaining portions become new (possibly broken) lines. A line that is entirely too close is removed completely. This guarantees that any two output lines coming from different input lines are at least ``min_distance`` apart — the longest lines always win the space. Proximity of a line with itself (e.g. a sharp U-turn) is not verified. Filtering by talhão is the caller's responsibility — pass only the lines of a single talhão to process them in isolation. Parameters ---------- lines : geopandas.GeoDataFrame Line geometries. Must be in a projected CRS (metres), since the distances are in CRS units. MultiLineStrings are exploded into single LineStrings before processing. min_distance : float The minimum distance (CRS units) allowed between two output lines. min_length : float, default 0.0 Resulting pieces shorter than this (CRS units) are discarded. Returns ------- geopandas.GeoDataFrame The pruned/broken lines, CRS preserved. Each piece inherits the attribute columns of its source line, so the output may have more rows than the input (lines broken into pieces) or fewer (lines removed entirely). Examples -------- >>> import shapelib >>> from shapelib.tools.lines import remove_close_segments >>> lines = shapelib.read_file("lines.shp") >>> lines.to_utm() >>> safe = remove_close_segments(lines, min_distance=2.0, min_length=5.0) Big-O ----- O(n²) proximity checks in the worst case (every line near every other); in practice bounded by the number of already-kept lines within ``min_distance`` of each candidate. """ if lines is None or len(lines) == 0: return lines geoms = lines.geometry geoms = geoms[geoms.notna() & ~geoms.is_empty].explode(index_parts=False) kept_geoms: list[LineString] = [] kept_labels: list = [] for label, geom in sorted( geoms.items(), key=lambda item: item[1].length, reverse=True ): near = shapely.dwithin(kept_geoms, geom, min_distance) if near.any(): forbidden = shapely.union_all( [g.buffer(min_distance) for g, is_near in zip(kept_geoms, near) if is_near] ) geom = geom.difference(forbidden) for piece in shapely.get_parts(geom): if isinstance(piece, LineString) and piece.length > min_length: kept_geoms.append(piece) kept_labels.append(label) attributes = lines.drop(columns=lines.geometry.name) return gpd.GeoDataFrame( attributes.loc[kept_labels].reset_index(drop=True), geometry=gpd.GeoSeries(kept_geoms, crs=lines.crs), )