diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c93a8..5fe6eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] - Mark the plugin as QGIS 4 compatible (`qgisMaximumVersion=4.99`) and fully scope PyQt6-required Qt enums in the map tool and settings dialog so they work on QGIS 4 / Qt 6 +- Accept polygon layers as the rotation reference in both the Processing algorithm and the interactive map tool; polygon boundary rings are treated as polylines for closest-segment math. **Breaking:** the Processing parameter `LINE_LAYER` has been renamed to `REFERENCE_LAYER` (saved models referencing the old key must be updated). The map tool prefers a line under the click when both a line and polygon overlap. ## [2.0.0] - 2026-06-06 - Add an interactive map tool: pick a reference line on the canvas, then click — or drag a rectangle — to rotate individual line/polygon features (or every feature in the rectangle across editable visible layers) parallel to it diff --git a/CLAUDE.md b/CLAUDE.md index d671566..0982b74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,14 +26,14 @@ QGIS discovery chain: PolygonsParallelToLine/__init__.py::classFactory → Plugin.initProcessing → Provider.loadAlgorithms → Algorithm ``` -`Algorithm` (`src/algorithm.py`) declares the processing UI (parameter IDs `LINE_LAYER`, `POLYGON_LAYER`, `LONGEST`, `NO_MULTI`, `DISTANCE`, `ANGLE`) and delegates to `PolygonsParallelToLine` in `src/pptl.py` via a `Params` dataclass (fields `by_longest`, `no_multi`, `distance`, `angle`). +`Algorithm` (`src/algorithm.py`) declares the processing UI (parameter IDs `REFERENCE_LAYER` — accepts line or polygon layers, `POLYGON_LAYER`, `LONGEST`, `NO_MULTI`, `DISTANCE`, `ANGLE`) and delegates to `PolygonsParallelToLine` in `src/pptl.py` via a `Params` dataclass (fields `by_longest`, `no_multi`, `distance`, `angle`). `PolygonsParallelToLine.process_polygon` per-feature pipeline: 1. Wrap feature in `Polygon` (`src/polygon.py`) — `geom`, `is_multi`, `center_xy`, `is_rotated`. -2. Wrap line source in `LineLayer` (`src/line.py`) — `QgsSpatialIndex` (with `FlagStoreFeatureGeometries`) plus an `id → QgsFeature` dict for `get_closest_line(point)`. -3. Short-circuit (no rotation) if `params.distance` is truthy and centroid→line distance exceeds it, or if `is_multi and no_multi`. -4. Hand off to `PolygonRotator` (`src/rotator.py`): finds the polygon vertex closest to the line, derives the two adjacent polygon segments, computes delta-azimuths against the closest line segment using helpers in `src/azimuth.py`. +2. Wrap reference source in `ReferenceLayer` (`src/reference.py`) — `QgsSpatialIndex` (with `FlagStoreFeatureGeometries`) plus an `id → QgsFeature` dict for `get_closest_feature(point)`. Polygon references are handled transparently: `closestSegmentWithContext` walks all rings (exterior + interior, all parts). +3. Short-circuit (no rotation) if `params.distance` is truthy and centroid→reference distance exceeds it, or if `is_multi and no_multi`. +4. Hand off to `PolygonRotator` (`src/rotator.py`): finds the polygon vertex closest to the reference, derives the two adjacent polygon segments, computes delta-azimuths against the closest reference segment using helpers in `src/azimuth.py`. 5. Rotation strategy: if both adjacent deltas are within `angle_threshold`, rotate by the longest segment (when `by_longest`) or by the smallest angle; if only one is within threshold, rotate by that one. Rotations within `ABSOLUTE_TOLERANCE` of 0 are skipped (avoids spurious `_rotated=True`). 6. Append a feature to the output sink with a `_rotated` boolean attribute (name in `src/const.py::COLUMN_NAME`). @@ -45,7 +45,7 @@ Invariants: ## Map tool -Alongside the Processing algorithm, `Plugin.initGui` registers a dedicated "Polygons Parallel to Line" `QToolBar` (objectName `PolygonsParallelToLineToolBar`) with two `QAction`s: the interactive parallelize toggle that activates `ParallelToLineMapTool` (`src/map_tool.py`) on the canvas, and a Settings action that opens `MapToolSettingsDialog` (`src/settings_dialog.py`) to edit `MapToolSettings` (`src/settings.py`). `MapToolSettings` owns live tool state (`by_longest`, `pick_reference_segment`, `pick_target_segment`) and persists each via `QSettings` under the `PolygonsParallelToLine/map_tool/` prefix, emitting `changed` on every write so the toolbar's checkable actions and the dialog's checkboxes stay in sync; the map tool reads these flags at each operation. The tool has two states: first click sets a reference line (highlighted with a `QgsRubberBand`); after that, a single click identifies and rotates one line/polygon target, while a click-and-drag draws a selection rectangle (`QgsRubberBand` polygon) and on release rotates every line/polygon feature whose geometry intersects the rectangle across all editable visible layers — wrapped per-layer in `beginEditCommand`/`endEditCommand`. Drag-rectangle is gated on the reference being set. Single clicks use `QgsMapToolIdentify.TopDownAll` so the tool walks through identify results top-down and picks the first one whose geometry type matches the current state's needs (line when no reference is set; line or polygon when one is). All cross-CRS work goes through `QgsCoordinateTransform` against `QgsProject.instance()`: the reference is stored in the map (canvas) CRS — transformed once at set time from the source layer's CRS — and `_reference_for_layer` re-projects it into each target layer's CRS at rotation time, while filter rectangles are transformed via `transformBoundingBox`. All math is delegated to `compute_parallel_geometry` in `src/parallelizer.py`, which reuses `Line`, `Segment`, `calc_delta_azimuth`, and `PolygonRotator` from the batch pipeline — pivot is the target's centroid. Right-click or `Esc` clears the reference (or cancels an in-progress drag). The toolbar additionally exposes two checkable actions (also mirrored as checkboxes in the settings dialog) to pick a single segment of the reference / target. With pick mode on, a single click identifies the segment closest to the click point; a drag-rectangle picks the segment of the topmost matching feature (reference role) or one segment per intersecting feature (target role) via `_pick_segment_in_rect`, which picks the segment with the largest overlap with the rectangle and falls back to the segment closest to the rectangle's center when no segment actually intersects (e.g., a rectangle sitting inside a polygon's interior). When the target-segment flag is on, the chosen `Segment` is passed through as `target_segment=` to `compute_parallel_geometry`, bypassing the usual `_pick_target_segment` selection. +Alongside the Processing algorithm, `Plugin.initGui` registers a dedicated "Polygons Parallel to Line" `QToolBar` (objectName `PolygonsParallelToLineToolBar`) with two `QAction`s: the interactive parallelize toggle that activates `ParallelToLineMapTool` (`src/map_tool.py`) on the canvas, and a Settings action that opens `MapToolSettingsDialog` (`src/settings_dialog.py`) to edit `MapToolSettings` (`src/settings.py`). `MapToolSettings` owns live tool state (`by_longest`, `pick_reference_segment`, `pick_target_segment`) and persists each via `QSettings` under the `PolygonsParallelToLine/map_tool/` prefix, emitting `changed` on every write so the toolbar's checkable actions and the dialog's checkboxes stay in sync; the map tool reads these flags at each operation. The tool has two states: first click sets a reference line (highlighted with a `QgsRubberBand`); after that, a single click identifies and rotates one line/polygon target, while a click-and-drag draws a selection rectangle (`QgsRubberBand` polygon) and on release rotates every line/polygon feature whose geometry intersects the rectangle across all editable visible layers — wrapped per-layer in `beginEditCommand`/`endEditCommand`. Drag-rectangle is gated on the reference being set. Single clicks use `QgsMapToolIdentify.TopDownAll` so the tool walks through identify results top-down and picks the first one whose geometry type matches the current state's needs (line or polygon when no reference is set, with line-first preference — a line under the click wins over a polygon; line or polygon when one is). The drag-rectangle reference path applies the same line-first preference across visible layers. All cross-CRS work goes through `QgsCoordinateTransform` against `QgsProject.instance()`: the reference is stored in the map (canvas) CRS — transformed once at set time from the source layer's CRS — and `_reference_for_layer` re-projects it into each target layer's CRS at rotation time, while filter rectangles are transformed via `transformBoundingBox`. All math is delegated to `compute_parallel_geometry` in `src/parallelizer.py`, which reuses `ReferenceFeature`, `Segment`, `calc_delta_azimuth`, and `PolygonRotator` from the batch pipeline — pivot is the target's centroid. Right-click or `Esc` clears the reference (or cancels an in-progress drag). The toolbar additionally exposes two checkable actions (also mirrored as checkboxes in the settings dialog) to pick a single segment of the reference / target. With pick mode on, a single click identifies the segment closest to the click point; a drag-rectangle picks the segment of the topmost matching feature (reference role) or one segment per intersecting feature (target role) via `_pick_segment_in_rect`, which picks the segment with the largest overlap with the rectangle and falls back to the segment closest to the rectangle's center when no segment actually intersects (e.g., a rectangle sitting inside a polygon's interior). When the target-segment flag is on, the chosen `Segment` is passed through as `target_segment=` to `compute_parallel_geometry`, bypassing the usual `_pick_target_segment` selection. ## Testing diff --git a/PolygonsParallelToLine/metadata.txt b/PolygonsParallelToLine/metadata.txt index 1956ba1..43452a1 100644 --- a/PolygonsParallelToLine/metadata.txt +++ b/PolygonsParallelToLine/metadata.txt @@ -2,13 +2,13 @@ name=Polygons Parallel to Line qgisMinimumVersion=3.22 qgisMaximumVersion=4.99 -description=Rotates polygons and lines to be parallel to a reference line, in batch (Processing) or interactively from the map canvas +description=Rotates polygons and lines to be parallel to a reference line or polygon, in batch (Processing) or interactively from the map canvas version=0.0.0 author=Andrii Liekariev email=elfpkck@gmail.com hasProcessingProvider=yes -about=This plugin rotates polygons (and lines) to be parallel to the nearest line segment. Two entry points are provided: a batch Processing algorithm that operates on whole layers, and an interactive map tool that lets you pick a reference line and then click individual line or polygon features to rotate them in place. It is useful when working with streets and buildings. Before using the plugin, we strongly recommend checking your data for geometric errors. A full description can be found at https://elfpkck.github.io/polygons_parallel_to_line/ +about=This plugin rotates polygons (and lines) to be parallel to the nearest reference segment; the reference can be a line or polygon layer (polygon boundary rings are treated as polylines). Two entry points are provided: a batch Processing algorithm that operates on whole layers, and an interactive map tool that lets you pick a reference line or polygon feature on the canvas and then click — or drag-rectangle — individual line/polygon features to rotate them in place. It is useful when working with streets and buildings. Before using the plugin, we strongly recommend checking your data for geometric errors. A full description can be found at https://elfpkck.github.io/polygons_parallel_to_line/ tracker=https://github.com/Elfpkck/polygons_parallel_to_line/issues repository=https://github.com/Elfpkck/polygons_parallel_to_line diff --git a/PolygonsParallelToLine/src/algorithm.py b/PolygonsParallelToLine/src/algorithm.py index 71ce530..8f97e0e 100644 --- a/PolygonsParallelToLine/src/algorithm.py +++ b/PolygonsParallelToLine/src/algorithm.py @@ -27,7 +27,7 @@ class Algorithm(QgsProcessingAlgorithm): OUTPUT_LAYER = "OUTPUT" - LINE_LAYER = "LINE_LAYER" + REFERENCE_LAYER = "REFERENCE_LAYER" POLYGON_LAYER = "POLYGON_LAYER" LONGEST = "LONGEST" NO_MULTI = "NO_MULTI" @@ -41,7 +41,7 @@ def name(self) -> str: return "pptl_algo" def displayName(self) -> str: # noqa: N802 - return "Polygons parallel to lines" + return "Polygons parallel to a reference layer" def group(self) -> str: return "Algorithms for vector layers" @@ -50,7 +50,7 @@ def groupId(self) -> str: # noqa: N802 return "pptl_group" def shortHelpString(self) -> str: # noqa: N802 - return "This plugin rotates polygons parallel to the lines" + return "Rotates polygons parallel to features in a reference layer (line or polygon)." def helpUrl(self) -> str: # noqa: N802 return "https://elfpkck.github.io/polygons_parallel_to_line/" @@ -63,7 +63,11 @@ def initAlgorithm(self, config: dict | None = None) -> None: # noqa: N802 ) ) self.addParameter( - QgsProcessingParameterFeatureSource(self.LINE_LAYER, "Line layer", [QgsProcessing.TypeVectorLine]) + QgsProcessingParameterFeatureSource( + self.REFERENCE_LAYER, + "Reference layer", + [QgsProcessing.TypeVectorLine, QgsProcessing.TypeVectorPolygon], + ) ) self.addParameter( QgsProcessingParameterFeatureSource(self.POLYGON_LAYER, "Polygon layer", [QgsProcessing.TypeVectorPolygon]) @@ -75,7 +79,7 @@ def initAlgorithm(self, config: dict | None = None) -> None: # noqa: N802 self.addParameter( QgsProcessingParameterNumber( self.DISTANCE, - "Max distance from line (in units of line layer CRS) (optional)", + "Max distance from reference (in units of reference layer CRS) (optional)", type=QgsProcessingParameterNumber.Double, minValue=0.0, defaultValue=0.0, @@ -112,7 +116,7 @@ def processAlgorithm( # noqa: N802 crs=polygon_layer.sourceCrs(), ) params = Params( - line_layer=self.parameterAsSource(parameters, self.LINE_LAYER, context), + reference_layer=self.parameterAsSource(parameters, self.REFERENCE_LAYER, context), polygon_layer=polygon_layer, by_longest=self.parameterAsBool(parameters, self.LONGEST, context), no_multi=self.parameterAsBool(parameters, self.NO_MULTI, context), diff --git a/PolygonsParallelToLine/src/line.py b/PolygonsParallelToLine/src/line.py deleted file mode 100644 index 25cd57b..0000000 --- a/PolygonsParallelToLine/src/line.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from functools import cached_property -from typing import TYPE_CHECKING - -from qgis.core import QgsProcessingException, QgsSpatialIndex - -if TYPE_CHECKING: - from qgis.core import ( - QgsFeature, - QgsGeometry, - QgsPoint, - QgsPointXY, - QgsProcessingFeatureSource, - ) - - -class Line: - def __init__(self, line_feature: QgsFeature): - self.geom: QgsGeometry = line_feature.geometry() - - def get_closest_segment(self, point_xy: QgsPointXY) -> Segment: - _, _, next_vertex_idx, _ = self.geom.closestSegmentWithContext(point_xy) - start, end = self.geom.vertexAt(next_vertex_idx - 1), self.geom.vertexAt(next_vertex_idx) - return Segment(start=start, end=end) - - -class LineLayer: - def __init__(self, line_layer: QgsProcessingFeatureSource): - self.id_line_map: dict[int, QgsFeature] = {x.id(): x for x in line_layer.getFeatures()} - self.spatial_index = QgsSpatialIndex(flags=QgsSpatialIndex.FlagStoreFeatureGeometries) - self.spatial_index.addFeatures(self.id_line_map.values()) - - def get_closest_line(self, point: QgsPointXY) -> Line: - closest_line_id = self.spatial_index.nearestNeighbor(point, 1) - - if not closest_line_id: - msg = f"No lines found near point {point}" - raise QgsProcessingException(msg) - - return Line(self.id_line_map[closest_line_id[0]]) - - -class Segment: - def __init__(self, start: QgsPoint, end: QgsPoint): - self.start = start - self.end = end - - @cached_property - def length(self) -> float: - return self.start.distance(self.end) - - @cached_property - def azimuth(self) -> float: - # QgsPoint.azimuth returns -180..180 - return self.start.azimuth(self.end) diff --git a/PolygonsParallelToLine/src/map_tool.py b/PolygonsParallelToLine/src/map_tool.py index a856285..9f1cad6 100644 --- a/PolygonsParallelToLine/src/map_tool.py +++ b/PolygonsParallelToLine/src/map_tool.py @@ -18,8 +18,8 @@ from qgis.PyQt.QtCore import QPoint, Qt # type: ignore[import-not-found] from qgis.PyQt.QtGui import QColor # type: ignore[import-not-found] -from .line import Line, Segment from .parallelizer import compute_parallel_geometry +from .reference import ReferenceFeature, Segment, iter_segments if TYPE_CHECKING: from qgis.core import QgsCoordinateReferenceSystem @@ -30,27 +30,21 @@ def _closest_segment_of(geom: QgsGeometry, point_xy: QgsPointXY) -> Segment: - feature = QgsFeature() - feature.setGeometry(geom) - return Line(feature).get_closest_segment(point_xy) + return ReferenceFeature.from_geometry(geom).get_closest_segment(point_xy) def _pick_segment_in_rect(geom: QgsGeometry, rect_geom: QgsGeometry, rect_center: QgsPointXY) -> Segment: best: Segment | None = None best_length = 0.0 - parts = geom.asGeometryCollection() or [geom] - for part in parts: - verts = list(part.vertices()) - for i in range(len(verts) - 1): - seg = Segment(start=verts[i], end=verts[i + 1]) - seg_geom = QgsGeometry.fromPolyline([seg.start, seg.end]) - clipped = seg_geom.intersection(rect_geom) - if clipped.isNull() or clipped.isEmpty(): - continue - length = clipped.length() - if length > best_length or best is None: - best_length = length - best = seg + for seg in iter_segments(geom): + seg_geom = QgsGeometry.fromPolyline([seg.start, seg.end]) + clipped = seg_geom.intersection(rect_geom) + if clipped.isNull() or clipped.isEmpty(): + continue + length = clipped.length() + if length > best_length or best is None: + best_length = length + best = seg if best is not None: return best return _closest_segment_of(geom, rect_center) @@ -61,10 +55,12 @@ def _pick_segment_in_rect(geom: QgsGeometry, rect_geom: QgsGeometry, rect_center class ParallelToLineMapTool(QgsMapToolIdentifyFeature): REFERENCE_COLOR = QColor(255, 140, 0, 200) + REFERENCE_FILL = QColor(255, 140, 0, 60) REFERENCE_WIDTH = 3 SELECTION_STROKE = QColor(0, 128, 255, 220) SELECTION_FILL = QColor(0, 128, 255, 50) DRAG_THRESHOLD_PX = 5 + REFERENCE_CLEARED_MSG = "Reference cleared. Click a line or polygon feature to set a new reference." def __init__(self, iface: QgisInterface, settings: MapToolSettings) -> None: super().__init__(iface.mapCanvas()) @@ -80,7 +76,7 @@ def __init__(self, iface: QgisInterface, settings: MapToolSettings) -> None: def activate(self) -> None: super().activate() self.setCursor(Qt.CursorShape.CrossCursor) - self._show_message("Click or drag-rectangle on a line to set the reference.", Qgis.Info) + self._show_message("Click or drag-rectangle on a line or polygon to set the reference.", Qgis.Info) def deactivate(self) -> None: self._clear_reference() @@ -94,7 +90,7 @@ def keyPressEvent(self, event: QKeyEvent) -> None: return if self.reference_geom is not None: self._clear_reference() - self._show_message("Reference cleared. Click a line feature to set a new reference.", Qgis.Info) + self._show_message(self.REFERENCE_CLEARED_MSG, Qgis.Info) else: self.iface.mapCanvas().unsetMapTool(self) return @@ -121,7 +117,7 @@ def canvasReleaseEvent(self, event: QgsMapMouseEvent) -> None: if event.button() == Qt.MouseButton.RightButton: self._cancel_drag() self._clear_reference() - self._show_message("Reference cleared. Click a line feature to set a new reference.", Qgis.Info) + self._show_message(self.REFERENCE_CLEARED_MSG, Qgis.Info) return if event.button() != Qt.MouseButton.LeftButton: @@ -154,33 +150,42 @@ def _handle_single_click(self, event: QgsMapMouseEvent) -> None: map_point = self.toMapCoordinates(event.pos()) + if self.reference_geom is None: + # Prefer a line feature under the click; fall back to a polygon only if + # no line was hit, so the existing line-reference UX never regresses. + for preferred in (QgsWkbTypes.LineGeometry, QgsWkbTypes.PolygonGeometry): + for result in results: + layer = result.mLayer + if not isinstance(layer, QgsVectorLayer): + continue + if layer.geometryType() != preferred: + continue + self._set_reference_from_feature(layer, result.mFeature, map_point) + return + return + for result in results: layer = result.mLayer if not isinstance(layer, QgsVectorLayer): continue - feature: QgsFeature = result.mFeature geom_type = layer.geometryType() - - if self.reference_geom is None: - if geom_type != QgsWkbTypes.LineGeometry: - continue - ref_geom = feature.geometry() - if self.settings.pick_reference_segment: - click_layer = self._point_in_layer_crs(map_point, layer) - segment = _closest_segment_of(ref_geom, click_layer) - ref_geom = QgsGeometry.fromPolylineXY([QgsPointXY(segment.start), QgsPointXY(segment.end)]) - self._set_reference(ref_geom, layer.crs()) - self._show_message( - "Reference set. Click or drag-rectangle to rotate line/polygon features.", - Qgis.Success, - ) - return - if geom_type not in (QgsWkbTypes.LineGeometry, QgsWkbTypes.PolygonGeometry): continue - self._rotate_target(layer, feature, geom_type, map_point) + self._rotate_target(layer, result.mFeature, geom_type, map_point) return + def _set_reference_from_feature(self, layer: QgsVectorLayer, feature: QgsFeature, map_point: QgsPointXY) -> None: + ref_geom = feature.geometry() + if self.settings.pick_reference_segment: + click_layer = self._point_in_layer_crs(map_point, layer) + segment = _closest_segment_of(ref_geom, click_layer) + ref_geom = QgsGeometry.fromPolylineXY([QgsPointXY(segment.start), QgsPointXY(segment.end)]) + self._set_reference(ref_geom, layer.crs()) + self._show_message( + "Reference set. Click or drag-rectangle to rotate line/polygon features.", + Qgis.Success, + ) + def _rotate_target( self, layer: QgsVectorLayer, @@ -225,35 +230,37 @@ def _set_reference_from_rect(self, map_rect: QgsRectangle) -> None: return canvas = self.iface.mapCanvas() - found: list[tuple[QgsVectorLayer, QgsFeature]] = [] - - for canvas_layer in canvas.layers(): - if not isinstance(canvas_layer, QgsVectorLayer): - continue - if canvas_layer.geometryType() != QgsWkbTypes.LineGeometry: - continue - layer_rect = self._transform_rect_to_layer(map_rect, canvas_layer) - rect_geom = QgsGeometry.fromRect(layer_rect) - request = QgsFeatureRequest().setFilterRect(layer_rect) - found.extend( - (canvas_layer, feature) - for feature in canvas_layer.getFeatures(request) - if feature.geometry().intersects(rect_geom) - ) + # Walk line layers first; only consider polygon layers if no line was found, + # so a polygon under a line does not steal the reference. + found: list[tuple[QgsVectorLayer, QgsFeature, QgsRectangle, QgsGeometry]] = [] + for preferred in (QgsWkbTypes.LineGeometry, QgsWkbTypes.PolygonGeometry): + for canvas_layer in canvas.layers(): + if not isinstance(canvas_layer, QgsVectorLayer): + continue + if canvas_layer.geometryType() != preferred: + continue + layer_rect = self._transform_rect_to_layer(map_rect, canvas_layer) + rect_geom = QgsGeometry.fromRect(layer_rect) + request = QgsFeatureRequest().setFilterRect(layer_rect) + found.extend( + (canvas_layer, feature, layer_rect, rect_geom) + for feature in canvas_layer.getFeatures(request) + if feature.geometry().intersects(rect_geom) + ) + if found: + break if not found: - self._show_message("No line feature in the selection.", Qgis.Info) + self._show_message("No line or polygon feature in the selection.", Qgis.Info) return - layer, feature = found[0] + layer, feature, layer_rect, rect_geom = found[0] ref_geom = feature.geometry() if self.settings.pick_reference_segment: - layer_rect = self._transform_rect_to_layer(map_rect, layer) - rect_geom = QgsGeometry.fromRect(layer_rect) segment = _pick_segment_in_rect(ref_geom, rect_geom, layer_rect.center()) ref_geom = QgsGeometry.fromPolylineXY([QgsPointXY(segment.start), QgsPointXY(segment.end)]) self._set_reference(ref_geom, layer.crs()) - suffix = f" ({len(found)} lines in selection; using topmost)" if len(found) > 1 else "" + suffix = f" ({len(found)} features in selection; using topmost)" if len(found) > 1 else "" self._show_message( f"Reference set{suffix}. Click or drag-rectangle to rotate line/polygon features.", Qgis.Success, @@ -311,12 +318,13 @@ def _rotate_layer_features( layer_rotated = 0 try: for feature in features: + feature_geom = feature.geometry() target_segment: Segment | None = None if pick_target and rect_geom is not None and rect_center is not None: - target_segment = _pick_segment_in_rect(feature.geometry(), rect_geom, rect_center) + target_segment = _pick_segment_in_rect(feature_geom, rect_geom, rect_center) rotated = compute_parallel_geometry( reference, - feature.geometry(), + feature_geom, kind, by_longest=self.settings.by_longest, target_segment=target_segment, @@ -378,9 +386,12 @@ def _set_reference(self, geom: QgsGeometry, source_crs: QgsCoordinateReferenceSy transform = QgsCoordinateTransform(source_crs, map_crs, QgsProject.instance()) reference.transform(transform) self.reference_geom = reference - rubber_band = QgsRubberBand(self.iface.mapCanvas(), QgsWkbTypes.LineGeometry) + wkb = QgsWkbTypes.geometryType(reference.wkbType()) + rubber_band = QgsRubberBand(self.iface.mapCanvas(), wkb) rubber_band.setColor(self.REFERENCE_COLOR) rubber_band.setWidth(self.REFERENCE_WIDTH) + if wkb == QgsWkbTypes.PolygonGeometry: + rubber_band.setFillColor(self.REFERENCE_FILL) rubber_band.setToGeometry(self.reference_geom) self.reference_rubber_band = rubber_band diff --git a/PolygonsParallelToLine/src/parallelizer.py b/PolygonsParallelToLine/src/parallelizer.py index 5b48149..81018a0 100644 --- a/PolygonsParallelToLine/src/parallelizer.py +++ b/PolygonsParallelToLine/src/parallelizer.py @@ -1,18 +1,15 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Literal +from typing import Literal from qgis.core import Qgis, QgsFeature, QgsGeometry from .azimuth import calc_delta_azimuth -from .line import Line, Segment from .polygon import Polygon +from .reference import ReferenceFeature, Segment, iter_segments from .rotator import PolygonRotator -if TYPE_CHECKING: - from collections.abc import Iterator - ABSOLUTE_TOLERANCE = 1e-8 @@ -24,21 +21,22 @@ def compute_parallel_geometry( by_longest: bool, target_segment: Segment | None = None, ) -> QgsGeometry | None: - reference_line = Line(_as_feature(reference_geom)) + reference = ReferenceFeature.from_geometry(reference_geom) if target_kind == "polygon" and target_segment is None: - target_feature = _as_feature(target_geom) + target_feature = QgsFeature() + target_feature.setGeometry(target_geom) poly = Polygon(target_feature) PolygonRotator( poly=poly, - closest_line=reference_line, + closest_reference=reference, angle_threshold=math.inf, by_longest=by_longest, ).rotate() return QgsGeometry(poly.geom) if poly.is_rotated else None centroid_xy = target_geom.centroid().asPoint() - ref_segment = reference_line.get_closest_segment(centroid_xy) + ref_segment = reference.get_closest_segment(centroid_xy) chosen_target_segment = ( target_segment if target_segment is not None @@ -55,22 +53,8 @@ def compute_parallel_geometry( return rotated -def _as_feature(geom: QgsGeometry) -> QgsFeature: - feature = QgsFeature() - feature.setGeometry(geom) - return feature - - def _pick_target_segment(target_geom: QgsGeometry, ref_segment: Segment, *, by_longest: bool) -> Segment: - segments = list(_iter_segments(target_geom)) + segments = list(iter_segments(target_geom)) if by_longest: return max(segments, key=lambda s: s.length) return min(segments, key=lambda s: abs(calc_delta_azimuth(ref_segment.azimuth, s.azimuth))) - - -def _iter_segments(geom: QgsGeometry) -> Iterator[Segment]: - parts = geom.asGeometryCollection() or [geom] - for part in parts: - verts = list(part.vertices()) - for i in range(len(verts) - 1): - yield Segment(start=verts[i], end=verts[i + 1]) diff --git a/PolygonsParallelToLine/src/polygon.py b/PolygonsParallelToLine/src/polygon.py index 558959d..bff6998 100644 --- a/PolygonsParallelToLine/src/polygon.py +++ b/PolygonsParallelToLine/src/polygon.py @@ -5,12 +5,12 @@ from qgis.core import Qgis, QgsGeometry, QgsProcessingException -from .line import Segment +from .reference import Segment if TYPE_CHECKING: from qgis.core import QgsFeature, QgsPoint, QgsPointXY - from .line import Line + from .reference import ReferenceFeature class Polygon: @@ -24,9 +24,9 @@ def __init__(self, polygon_feature: QgsFeature): def center_xy(self) -> QgsPointXY: return self.geom.centroid().asPoint() - def get_closest_vertex(self, closest_line: Line) -> QgsPoint: - nearest_point_on_line_geom = closest_line.geom.nearestPoint(self.geom) - _, closest_vertex_idx = self.geom.closestVertexWithContext(nearest_point_on_line_geom.asPoint()) + def get_closest_vertex(self, closest_reference: ReferenceFeature) -> QgsPoint: + nearest_point_on_ref = closest_reference.geom.nearestPoint(self.geom) + _, closest_vertex_idx = self.geom.closestVertexWithContext(nearest_point_on_ref.asPoint()) return self.geom.vertexAt(closest_vertex_idx) def get_adjacent_segments(self, target_vertex: QgsPoint) -> tuple[Segment, Segment]: diff --git a/PolygonsParallelToLine/src/pptl.py b/PolygonsParallelToLine/src/pptl.py index ef0358e..c0c4d33 100644 --- a/PolygonsParallelToLine/src/pptl.py +++ b/PolygonsParallelToLine/src/pptl.py @@ -13,8 +13,8 @@ ) from .const import COLUMN_NAME -from .line import LineLayer from .polygon import Polygon +from .reference import ReferenceLayer from .rotator import PolygonRotator if TYPE_CHECKING: @@ -23,7 +23,7 @@ @dataclasses.dataclass class Params: - line_layer: QgsProcessingFeatureSource + reference_layer: QgsProcessingFeatureSource polygon_layer: QgsProcessingFeatureSource by_longest: bool no_multi: bool @@ -40,8 +40,8 @@ def __init__(self, feedback: QgsProcessingFeedback, params: Params): self.total_number: int = self.params.polygon_layer.featureCount() @cached_property - def line_layer(self) -> LineLayer: - return LineLayer(self.params.line_layer) + def reference_layer(self) -> ReferenceLayer: + return ReferenceLayer(self.params.reference_layer) def run(self) -> None: # pydevd_pycharm.settrace("127.0.0.1", port=53100, stdoutToServer=True, stderrToServer=True) # noqa: ERA001 @@ -70,15 +70,15 @@ def process_polygon(self, polygon: QgsFeature) -> QgsFeature: if self.params.no_multi and poly.is_multi: return self.create_new_feature(poly) - # Selected by centroid distance; an edge of the polygon may be nearer to a different line. - closest_line = self.line_layer.get_closest_line(poly.center_xy) + # Selected by centroid distance; an edge of the polygon may be nearer to a different reference. + closest_reference = self.reference_layer.get_closest_feature(poly.center_xy) - if self.params.distance and closest_line.geom.distance(poly.geom) > self.params.distance: + if self.params.distance and closest_reference.geom.distance(poly.geom) > self.params.distance: return self.create_new_feature(poly) PolygonRotator( poly=poly, - closest_line=closest_line, + closest_reference=closest_reference, angle_threshold=self.params.angle, by_longest=self.params.by_longest, ).rotate() diff --git a/PolygonsParallelToLine/src/provider.py b/PolygonsParallelToLine/src/provider.py index 0a5ac05..53ab99c 100644 --- a/PolygonsParallelToLine/src/provider.py +++ b/PolygonsParallelToLine/src/provider.py @@ -18,7 +18,7 @@ def id(self) -> str: return "pptl" def name(self) -> str: - return "Polygons parallel to lines" + return "Polygons parallel to a reference layer" def icon(self) -> QIcon: return super().icon() diff --git a/PolygonsParallelToLine/src/reference.py b/PolygonsParallelToLine/src/reference.py new file mode 100644 index 0000000..9771cfb --- /dev/null +++ b/PolygonsParallelToLine/src/reference.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from qgis.core import QgsPoint, QgsProcessingException, QgsSpatialIndex, QgsWkbTypes + +if TYPE_CHECKING: + from collections.abc import Iterator + + from qgis.core import ( + QgsFeature, + QgsGeometry, + QgsPointXY, + QgsProcessingFeatureSource, + ) + + +class ReferenceFeature: + def __init__(self, feature: QgsFeature): + self.geom: QgsGeometry = feature.geometry() + + @classmethod + def from_geometry(cls, geom: QgsGeometry) -> ReferenceFeature: + obj = cls.__new__(cls) + obj.geom = geom + return obj + + def get_closest_segment(self, point_xy: QgsPointXY) -> Segment: + sqr_dist, _, next_vertex_idx, _ = self.geom.closestSegmentWithContext(point_xy) + if sqr_dist < 0 or next_vertex_idx <= 0: + msg = f"Reference geometry has no valid segment near {point_xy}" + raise QgsProcessingException(msg) + start, end = self.geom.vertexAt(next_vertex_idx - 1), self.geom.vertexAt(next_vertex_idx) + return Segment(start=start, end=end) + + +class ReferenceLayer: + def __init__(self, source: QgsProcessingFeatureSource): + self.id_feature_map: dict[int, QgsFeature] = {x.id(): x for x in source.getFeatures()} + self.spatial_index = QgsSpatialIndex(flags=QgsSpatialIndex.FlagStoreFeatureGeometries) + self.spatial_index.addFeatures(self.id_feature_map.values()) + + def get_closest_feature(self, point: QgsPointXY) -> ReferenceFeature: + closest_id = self.spatial_index.nearestNeighbor(point, 1) + + if not closest_id: + msg = f"No reference features found near point {point}" + raise QgsProcessingException(msg) + + return ReferenceFeature(self.id_feature_map[closest_id[0]]) + + +class Segment: + def __init__(self, start: QgsPoint, end: QgsPoint): + self.start = start + self.end = end + + @cached_property + def length(self) -> float: + return self.start.distance(self.end) + + @cached_property + def azimuth(self) -> float: + # QgsPoint.azimuth returns -180..180 + return self.start.azimuth(self.end) + + +def iter_segments(geom: QgsGeometry) -> Iterator[Segment]: + # Per-ring iteration so polygons with interior rings (or multipart geometries) + # never produce a spurious segment that jumps between rings/parts. + for part in geom.asGeometryCollection() or [geom]: + gtype = QgsWkbTypes.geometryType(part.wkbType()) + if gtype == QgsWkbTypes.LineGeometry: + rings = part.asMultiPolyline() if part.isMultipart() else [part.asPolyline()] + elif gtype == QgsWkbTypes.PolygonGeometry: + polygons = part.asMultiPolygon() if part.isMultipart() else [part.asPolygon()] + rings = [ring for poly in polygons for ring in poly] + else: + continue + for ring in rings: + for i in range(len(ring) - 1): + yield Segment(start=QgsPoint(ring[i]), end=QgsPoint(ring[i + 1])) diff --git a/PolygonsParallelToLine/src/rotator.py b/PolygonsParallelToLine/src/rotator.py index ab8d8f5..3eb9fee 100644 --- a/PolygonsParallelToLine/src/rotator.py +++ b/PolygonsParallelToLine/src/rotator.py @@ -8,22 +8,22 @@ from .azimuth import calc_delta_azimuth if TYPE_CHECKING: - from .line import Line from .polygon import Polygon + from .reference import ReferenceFeature class PolygonRotator: ABSOLUTE_TOLERANCE = 1e-8 - def __init__(self, poly: Polygon, closest_line: Line, angle_threshold: float, *, by_longest: bool): + def __init__(self, poly: Polygon, closest_reference: ReferenceFeature, angle_threshold: float, *, by_longest: bool): self.poly = poly self.angle_threshold = angle_threshold self.by_longest = by_longest - poly_closest_vertex = poly.get_closest_vertex(closest_line) + poly_closest_vertex = poly.get_closest_vertex(closest_reference) self.prev_poly_segment, self.next_poly_segment = poly.get_adjacent_segments(poly_closest_vertex) - line_segment = closest_line.get_closest_segment(QgsPointXY(poly_closest_vertex)) - self.prev_delta_azimuth = calc_delta_azimuth(line_segment.azimuth, self.prev_poly_segment.azimuth) - self.next_delta_azimuth = calc_delta_azimuth(line_segment.azimuth, self.next_poly_segment.azimuth) + ref_segment = closest_reference.get_closest_segment(QgsPointXY(poly_closest_vertex)) + self.prev_delta_azimuth = calc_delta_azimuth(ref_segment.azimuth, self.prev_poly_segment.azimuth) + self.next_delta_azimuth = calc_delta_azimuth(ref_segment.azimuth, self.next_poly_segment.azimuth) def rotate(self) -> None: prev_within = abs(self.prev_delta_azimuth) <= self.angle_threshold diff --git a/README.md b/README.md index dcfce87..0cfccc9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Polygons Parallel to Line - QGIS Python Plugin -A QGIS plugin that rotates polygons (and lines) to be parallel to a reference line. Two entry points: a batch Processing algorithm that operates on whole layers, and an interactive map tool that lets you pick a reference line on the canvas and then click — or drag-rectangle — individual line/polygon features to rotate them in place. +A QGIS plugin that rotates polygons (and lines) to be parallel to a reference feature (line or polygon — polygon boundary rings are treated as polylines). Two entry points: a batch Processing algorithm that operates on whole layers, and an interactive map tool that lets you pick a reference feature on the canvas and then click — or drag-rectangle — individual line/polygon features to rotate them in place. [Polygons Parallel to Line Plugin on QGIS Plugins Web Portal](https://plugins.qgis.org/plugins/PolygonsParallelToLine/) @@ -42,8 +42,8 @@ A QGIS plugin that rotates polygons (and lines) to be parallel to a reference li ## Quick Start ### Batch (Processing algorithm) -1. **Access the Plugin**: Go to **Processing** → **Toolbox** → **Polygons parallel to lines** -2. **Select Input Layers**: Choose your polygon and line layers +1. **Access the Plugin**: Go to **Processing** → **Toolbox** → **Polygons parallel to a reference layer** +2. **Select Input Layers**: Choose your polygon layer and a reference layer (line or polygon) 3. **Configure Parameters**: Set distance and angle thresholds as needed 4. **Run**: Execute the algorithm to generate aligned polygons @@ -51,7 +51,7 @@ A QGIS plugin that rotates polygons (and lines) to be parallel to a reference li ### Interactive (map tool) 1. Open the **Polygons Parallel to Line** toolbar (also reachable from **Vector** → **Polygons Parallel to Line**) and click the **Parallel to Line (interactive)** action. -2. Click a line feature — or drag a rectangle over one — to set it as the reference. The reference is highlighted on the canvas. +2. Click a line or polygon feature — or drag a rectangle over one — to set it as the reference. The reference is highlighted on the canvas (polygons get an outline plus translucent fill); if both a line and a polygon are under the click, the line wins. 3. Toggle editing on the layers you want to modify, then either click a single line/polygon to rotate it, or drag a rectangle to rotate every line/polygon feature that intersects it across all editable visible layers. 4. Use the **Settings…** action on the same toolbar to choose between rotation strategies (currently *Rotate by longest segment*). Settings persist via `QSettings`. 5. Right-click or press **Esc** to clear the reference; press **Esc** again to deactivate the tool. @@ -59,7 +59,7 @@ A QGIS plugin that rotates polygons (and lines) to be parallel to a reference li ## Features ✅ **Two modes**: batch Processing algorithm for whole layers, plus an interactive map-canvas tool for one-off rotations -✅ **Automatic Polygon Rotation**: Rotates polygons to align with the nearest line +✅ **Automatic Polygon Rotation**: Rotates polygons to align with the nearest reference edge (line or polygon ring) ✅ **Line-target support (interactive)**: the map tool can rotate line features too, not just polygons ✅ **Bulk drag-rectangle**: rotate every line/polygon intersecting a rectangle across all editable visible layers — wrapped per-layer in undo-able edit commands ✅ **CRS-aware**: reference and targets across layers in different CRSes are reconciled via `QgsCoordinateTransform` @@ -72,15 +72,15 @@ A QGIS plugin that rotates polygons (and lines) to be parallel to a reference li The plugin processes each polygon using the following steps: -1. **Closest Line Selection**: Finds the line whose feature is the nearest neighbor of the polygon centroid +1. **Closest Reference Selection**: Finds the reference feature (line or polygon) whose geometry is the nearest neighbor of the polygon centroid -2. **Distance Check**: If `Max distance from line` > 0 and the polygon-to-line geometry distance (closest edge of the polygon to the closest line) exceeds it → skip rotation +2. **Distance Check**: If `Max distance from reference` > 0 and the polygon-to-reference geometry distance exceeds it → skip rotation (note: when the target polygon overlaps the reference polygon, the distance is 0 and rotation runs even at small thresholds) -3. **Vertex Analysis**: Identifies the polygon vertex closest to the nearest line +3. **Vertex Analysis**: Identifies the polygon vertex closest to the nearest reference 4. **Segment Evaluation**: Takes the two polygon segments adjacent to that vertex -5. **Angle Calculation**: Computes the signed angle (delta azimuth) between the closest line segment and each adjacent polygon segment +5. **Angle Calculation**: Computes the signed angle (delta azimuth) between the closest reference segment (any ring of a polygon reference counts) and each adjacent polygon segment 6. **Rotation Decision** (each delta is compared against `Max angle`): - If both deltas are within `Max angle`: @@ -104,11 +104,11 @@ The plugin processes each polygon using the following steps: ## Configuration -### Max Distance from Line +### Max Distance from Reference - **Type**: Float (optional) - **Range**: ≥ 0.0 - **Default**: 0.0 (processes all polygons) -- **Unit**: Line layer CRS units +- **Unit**: Reference layer CRS units When set to 0.0, all polygons are processed regardless of distance. @@ -160,7 +160,7 @@ When set to 0.0, all polygons are processed regardless of distance. ⚠️ **Important**: Validate and fix geometry errors before running the plugin for optimal results. ### Recommended Workflow -1. **Prepare Data**: Ensure polygon and line layers are in the same CRS +1. **Prepare Data**: Ensure polygon and reference layers are in the same CRS 2. **Fix Geometries**: Use **Processing Toolbox** → **Vector geometry** → **Fix Geometries** 3. **Test Parameters**: Start with default settings on a small dataset 4. **Batch Process**: Apply to full dataset with optimized parameters diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 265fe07..81b0cb7 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -14,10 +14,12 @@ def algorithm_instance(): def test_algorithm_initialization(algorithm_instance): """Test if the Algorithm instance initializes correctly.""" assert algorithm_instance.name() == "pptl_algo" - assert algorithm_instance.displayName() == "Polygons parallel to lines" + assert algorithm_instance.displayName() == "Polygons parallel to a reference layer" assert algorithm_instance.group() == "Algorithms for vector layers" assert algorithm_instance.groupId() == "pptl_group" - assert algorithm_instance.shortHelpString() == "This plugin rotates polygons parallel to the lines" + assert algorithm_instance.shortHelpString() == ( + "Rotates polygons parallel to features in a reference layer (line or polygon)." + ) assert algorithm_instance.helpUrl() == "https://elfpkck.github.io/polygons_parallel_to_line/" diff --git a/tests/test_main_functionality.py b/tests/test_main_functionality.py index 5acfc70..4bb876d 100644 --- a/tests/test_main_functionality.py +++ b/tests/test_main_functionality.py @@ -157,7 +157,7 @@ def test_main_functionality( add_features(vector_layer=poly_layer, wkt_geometries=polys) params = { - "LINE_LAYER": line_layer, + "REFERENCE_LAYER": line_layer, "POLYGON_LAYER": poly_layer, "LONGEST": longest, "NO_MULTI": no_multi, @@ -177,9 +177,9 @@ def test_main_functionality( assert [x[const.COLUMN_NAME] for x in output_layer.getFeatures()] == _rotated -def test_no_multi_skips_multipolygons_when_line_layer_empty(qgis_processing, add_features): +def test_no_multi_skips_multipolygons_when_reference_layer_empty(qgis_processing, add_features): # Regression: with no_multi=True, multipolygons must be skipped without touching the - # (empty) line layer; previously the line lookup ran first and would raise. + # (empty) reference layer; previously the reference lookup ran first and would raise. line_layer = QgsVectorLayer("linestring", "temp_line", "memory") poly_layer = QgsVectorLayer("polygon", "temp_poly", "memory") @@ -192,7 +192,7 @@ def test_no_multi_skips_multipolygons_when_line_layer_empty(qgis_processing, add ) params = { - "LINE_LAYER": line_layer, + "REFERENCE_LAYER": line_layer, "POLYGON_LAYER": poly_layer, "LONGEST": False, "NO_MULTI": True, @@ -207,6 +207,46 @@ def test_no_multi_skips_multipolygons_when_line_layer_empty(qgis_processing, add assert [x[const.COLUMN_NAME] for x in output_layer.getFeatures()] == [False, False] +def test_polygon_reference_matches_equivalent_line_reference(qgis_processing, add_features): + # A polygon reference whose exterior ring matches a line reference's coordinate + # sequence should produce the same rotated outputs as the line reference. + ring_coords = "0 0, 100 5, 200 -3, 300 4" + line_wkt = f"LineString ({ring_coords}, 0 0)" + poly_wkt = f"Polygon (({ring_coords}, 0 0))" + target_polygons = ( + "Polygon ((50 50, 60 50, 60 60, 50 60, 50 50))", + "Polygon ((150 80, 170 82, 168 100, 148 98, 150 80))", + "Polygon ((250 70, 270 71, 269 90, 249 89, 250 70))", + ) + + def _run(reference_wkt: str) -> list[str]: + ref_layer = QgsVectorLayer( + "linestring" if reference_wkt.startswith("LineString") else "polygon", + "ref", + "memory", + ) + add_features(vector_layer=ref_layer, wkt_geometries=(reference_wkt,)) + poly_layer = QgsVectorLayer("polygon", "tgt", "memory") + add_features(vector_layer=poly_layer, wkt_geometries=target_polygons) + params = { + "REFERENCE_LAYER": ref_layer, + "POLYGON_LAYER": poly_layer, + "LONGEST": False, + "NO_MULTI": False, + "DISTANCE": 0.0, + "ANGLE": 89.9, + "OUTPUT": QgsProcessingOutputLayerDefinition("TEMPORARY_OUTPUT"), + } + context = QgsProcessingContext() + result = processing.run(algOrName=Algorithm(), parameters=params, context=context) + out = context.getMapLayer(result[Algorithm.OUTPUT_LAYER]) + return [f.geometry().asWkt() for f in out.getFeatures()] + + line_outputs = _run(line_wkt) + polygon_outputs = _run(poly_wkt) + assert line_outputs == polygon_outputs + + @pytest.fixture(scope="module") def add_features(): def add_wkt_features_to_layer(vector_layer: QgsVectorLayer, wkt_geometries: tuple[str, ...]) -> None: diff --git a/tests/test_parallelizer.py b/tests/test_parallelizer.py index d61cafc..5ab4254 100644 --- a/tests/test_parallelizer.py +++ b/tests/test_parallelizer.py @@ -6,8 +6,8 @@ from qgis.core import QgsGeometry, QgsPoint, QgsPointXY from PolygonsParallelToLine.src.azimuth import calc_delta_azimuth -from PolygonsParallelToLine.src.line import Segment from PolygonsParallelToLine.src.parallelizer import compute_parallel_geometry +from PolygonsParallelToLine.src.reference import Segment def _delta_to_reference(target_wkt: str, reference_wkt: str, *, by_longest: bool) -> float: diff --git a/tests/test_performance.py b/tests/test_performance.py index c7fbeb5..b7972e8 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -64,7 +64,7 @@ def test_perf_smoke_grid_1024_polygons(qgis_processing, add_features): add_features(vector_layer=poly_layer, wkt_geometries=tuple(_grid_polygons())) params = { - "LINE_LAYER": line_layer, + "REFERENCE_LAYER": line_layer, "POLYGON_LAYER": poly_layer, "LONGEST": False, "NO_MULTI": False, diff --git a/tests/test_provider.py b/tests/test_provider.py index 9781a26..639772b 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -18,7 +18,7 @@ def test_provider_id(provider_instance): def test_provider_name(provider_instance): """Test if the provider name is correct.""" - assert provider_instance.name() == "Polygons parallel to lines" + assert provider_instance.name() == "Polygons parallel to a reference layer" def test_provider_load_algorithms(provider_instance):