Skip to content

Commit 5527356

Browse files
author
Daniel
committed
feature: Create atlas coverage along centerline
1 parent 5b473cf commit 5527356

File tree

3 files changed

+288
-3
lines changed

3 files changed

+288
-3
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import os
4+
from qgis.PyQt.QtCore import QCoreApplication
5+
from qgis.core import (
6+
QgsProcessing,
7+
QgsProcessingParameterVectorLayer,
8+
QgsProcessingParameterNumber,
9+
QgsProcessingParameterFeatureSink,
10+
QgsProperty,
11+
QgsProcessingException,
12+
QgsWkbTypes # Import WkbTypes
13+
)
14+
import processing
15+
16+
from .base_algorithm import GvBaseProcessingAlgorithms
17+
18+
class CreateAtlasCoverageAlgorithm(GvBaseProcessingAlgorithms):
19+
"""
20+
Creates atlas coverage points and polygons from a centerline.
21+
22+
This algorithm takes a line layer and layout parameters
23+
to generate a point layer for atlas control and a polygon
24+
layer representing the coverage of each atlas page.
25+
"""
26+
27+
# --- Define parameter and output names ---
28+
INPUT_LINE = 'INPUT_LINE'
29+
INPUT_SCALE = 'INPUT_SCALE'
30+
INPUT_PAPER_LONG_MM = 'INPUT_PAPER_LONG_MM'
31+
INPUT_PAPER_SHORT_MM = 'INPUT_PAPER_SHORT_MM'
32+
INPUT_OVERLAP = 'INPUT_OVERLAP'
33+
34+
OUTPUT_POINTS = 'OUTPUT_POINTS'
35+
OUTPUT_POLYGONS = 'OUTPUT_POLYGONS'
36+
37+
def tr(self, string):
38+
"""
39+
Returns a translatable string with the self.tr() function.
40+
"""
41+
return QCoreApplication.translate('Processing', string)
42+
43+
def createInstance(self):
44+
return CreateAtlasCoverageAlgorithm()
45+
46+
def name(self):
47+
"""
48+
Returns the unique algorithm name.
49+
"""
50+
return 'create_atlas_coverage'
51+
52+
def displayName(self):
53+
"""
54+
Returns the human-readable name.
55+
"""
56+
return self.tr('Create Atlas Coverage')
57+
58+
def group(self):
59+
"""
60+
Returns the group name.
61+
"""
62+
return self.tr('Geovita') # Or your preferred group name
63+
64+
def groupId(self):
65+
"""
66+
Returns the group ID.
67+
"""
68+
return 'geovita' # Must match your provider ID
69+
70+
def iconPath(self):
71+
"""
72+
Sets the icon for the algorithm.
73+
"""
74+
icon_path = os.path.join(os.path.dirname(__file__), '..', 'icons', 'atlas.png')
75+
if not os.path.exists(icon_path):
76+
# Fallback to the main plugin icon if 'atlas.png' doesn't exist
77+
icon_path = os.path.join(os.path.dirname(__file__), '..', 'icons', 'geovita.ico')
78+
return icon_path
79+
80+
81+
def shortHelpString(self):
82+
"""
83+
Returns a brief description for the algorithm's help panel.
84+
"""
85+
return self.tr('Generates atlas control points and polygon coverage '
86+
'along a line based on paper size, scale, and overlap.\n\n'
87+
'The "long axis" of the paper will be aligned parallel to the line.')
88+
89+
def initAlgorithm(self, config=None):
90+
"""
91+
Defines the inputs and outputs.
92+
"""
93+
94+
# --- Inputs ---
95+
self.addParameter(
96+
QgsProcessingParameterVectorLayer(
97+
self.INPUT_LINE,
98+
self.tr('Input Centerline'),
99+
[QgsProcessing.TypeVectorLine]
100+
)
101+
)
102+
param_line = self.parameterDefinition(self.INPUT_LINE)
103+
param_line.setHelp(self.tr('The road or rail line to follow.'))
104+
105+
106+
param_scale = QgsProcessingParameterNumber(
107+
self.INPUT_SCALE,
108+
self.tr('Map Scale (e.g., 1000)'),
109+
QgsProcessingParameterNumber.Integer,
110+
defaultValue=1000
111+
)
112+
param_scale.setHelp(self.tr('Enter the denominator of the map scale (e.g., 1000 for 1:1000).'))
113+
self.addParameter(param_scale)
114+
115+
param_long = QgsProcessingParameterNumber(
116+
self.INPUT_PAPER_LONG_MM,
117+
self.tr('Paper Long Axis (Usable mm)'),
118+
QgsProcessingParameterNumber.Double,
119+
defaultValue=410.0
120+
)
121+
param_long.setHelp(self.tr('The dimension of the paper you want aligned parallel to the road (in millimeters).\n\nExample: For a 420mm A3 sheet with 5mm margins, use 410.'))
122+
self.addParameter(param_long)
123+
124+
param_short = QgsProcessingParameterNumber(
125+
self.INPUT_PAPER_SHORT_MM,
126+
self.tr('Paper Short Axis (Usable mm)'),
127+
QgsProcessingParameterNumber.Double,
128+
defaultValue=287.0
129+
)
130+
param_short.setHelp(self.tr('The dimension of the paper perpendicular to the road (in millimeters).\n\Example: For a 297mm A3 sheet with 5mm margins, use 287.'))
131+
self.addParameter(param_short)
132+
133+
param_overlap = QgsProcessingParameterNumber(
134+
self.INPUT_OVERLAP,
135+
self.tr('Overlap (in meters)'),
136+
QgsProcessingParameterNumber.Double,
137+
defaultValue=50.0
138+
)
139+
param_overlap.setHelp(self.tr('The real-world overlap distance between atlas pages (in meters).'))
140+
self.addParameter(param_overlap)
141+
142+
# --- Outputs ---
143+
144+
param_out_pts = QgsProcessingParameterFeatureSink(
145+
self.OUTPUT_POINTS,
146+
self.tr('Atlas Points')
147+
)
148+
param_out_pts.setHelp(self.tr('Output point layer to be used as the atlas coverage layer. Contains the "angle" field for rotation.'))
149+
self.addParameter(param_out_pts)
150+
151+
param_out_poly = QgsProcessingParameterFeatureSink(
152+
self.OUTPUT_POLYGONS,
153+
self.tr('Atlas Coverage Polygons')
154+
)
155+
param_out_poly.setHelp(self.tr('Output polygon layer showing the footprint of each atlas page. Useful for an index map.'))
156+
self.addParameter(param_out_poly)
157+
158+
159+
def processAlgorithm(self, parameters, context, feedback):
160+
"""
161+
The main processing logic.
162+
"""
163+
164+
# --- 1. Get Inputs ---
165+
source_line = self.parameterAsSource(parameters, self.INPUT_LINE, context)
166+
scale = self.parameterAsDouble(parameters, self.INPUT_SCALE, context)
167+
paper_long_mm = self.parameterAsDouble(parameters, self.INPUT_PAPER_LONG_MM, context)
168+
paper_short_mm = self.parameterAsDouble(parameters, self.INPUT_PAPER_SHORT_MM, context)
169+
overlap = self.parameterAsDouble(parameters, self.INPUT_OVERLAP, context)
170+
171+
# Check for valid inputs
172+
if not source_line:
173+
raise QgsProcessingException(self.tr('Invalid input layer. Please select a line layer.'))
174+
175+
# --- 2. Run Calculations ---
176+
real_long_m = (paper_long_mm * scale) / 1000.0
177+
real_short_m = (paper_short_mm * scale) / 1000.0
178+
step_distance = real_long_m - overlap
179+
180+
feedback.pushInfo(self.tr(f'Real-world page size (LxW): {real_long_m}m x {real_short_m}m'))
181+
feedback.pushInfo(self.tr(f'Point step distance (with overlap): {step_distance}m'))
182+
183+
if step_distance <= 0:
184+
raise QgsProcessingException(self.tr('Overlap ({0}m) is larger than or equal to the real-world long axis ({1}m). Please reduce overlap or check paper size.').format(overlap, real_long_m))
185+
186+
if feedback.isCanceled():
187+
return {}
188+
189+
# --- 3. Run "Points along geometry" ---
190+
feedback.pushInfo(self.tr('Generating points along line...'))
191+
192+
# If input is 3D (25D), we must make sure the output is 2D for the atlas
193+
# This removes the Z and M values from the geometry
194+
force_2d_result = processing.run(
195+
'native:dropmzvalues',
196+
{
197+
'INPUT': parameters[self.INPUT_LINE],
198+
'OUTPUT': 'memory:'
199+
},
200+
context=context,
201+
feedback=feedback,
202+
is_child_algorithm=True
203+
)
204+
205+
line_2d_layer = context.takeResultLayer(force_2d_result['OUTPUT'])
206+
207+
points_result = processing.run(
208+
'native:pointsalonglines',
209+
{
210+
'INPUT': line_2d_layer, # Use the 2D line
211+
'DISTANCE': step_distance,
212+
'OUTPUT': 'memory:'
213+
},
214+
context=context,
215+
feedback=feedback,
216+
is_child_algorithm=True
217+
)
218+
219+
if feedback.isCanceled():
220+
return {}
221+
222+
points_layer = context.takeResultLayer(points_result['OUTPUT'])
223+
224+
if points_layer.featureCount() == 0:
225+
feedback.pushWarning(self.tr('No points were created. The input line might be shorter than the calculated step distance.'))
226+
return {self.OUTPUT_POINTS: None, self.OUTPUT_POLYGONS: None}
227+
228+
# --- 4. Define POINTS Sink and Add Features ---
229+
#
230+
# *** THIS IS THE FIRST CORRECTED BLOCK ***
231+
# The argument order is (fields, wkbType, crs)
232+
#
233+
(sink_points, dest_id_points) = self.parameterAsSink(
234+
parameters, self.OUTPUT_POINTS, context,
235+
points_layer.fields(), # Argument 4: Fields
236+
points_layer.wkbType(), # Argument 5: WKB Type
237+
points_layer.crs() # Argument 6: CRS
238+
)
239+
sink_points.addFeatures(points_layer.getFeatures())
240+
241+
242+
# --- 5. Run "Rectangles, Ovals, Diamonds" ---
243+
feedback.pushInfo(self.tr('Creating coverage polygons...'))
244+
polygons_result = processing.run(
245+
'native:rectanglesovalsdiamonds',
246+
{
247+
'INPUT': points_layer, # Use the memory layer
248+
'SHAPE': 0, # 0 = Rectangle
249+
'WIDTH': real_short_m, # Short side is the "Width"
250+
'HEIGHT': real_long_m, # Long side is the "Height"
251+
'ROTATION': QgsProperty.fromField('angle'), # Use data-defined override
252+
'OUTPUT': 'memory:'
253+
},
254+
context=context,
255+
feedback=feedback,
256+
is_child_algorithm=True
257+
)
258+
259+
if feedback.isCanceled():
260+
return {}
261+
262+
polygons_layer = context.takeResultLayer(polygons_result['OUTPUT'])
263+
264+
# --- 6. Define POLYGONS Sink and Add Features ---
265+
#
266+
# *** THIS IS THE SECOND CORRECTED BLOCK ***
267+
# The argument order is (fields, wkbType, crs)
268+
#
269+
(sink_polygons, dest_id_polygons) = self.parameterAsSink(
270+
parameters, self.OUTPUT_POLYGONS, context,
271+
polygons_layer.fields(), # Argument 4: Fields
272+
polygons_layer.wkbType(), # Argument 5: WKB Type
273+
polygons_layer.crs() # Argument 6: CRS
274+
)
275+
sink_polygons.addFeatures(polygons_layer.getFeatures())
276+
277+
278+
# --- 7. Return Outputs ---
279+
feedback.pushInfo(self.tr('Atlas coverage created successfully.'))
280+
return {
281+
self.OUTPUT_POINTS: dest_id_points,
282+
self.OUTPUT_POLYGONS: dest_id_polygons
283+
}

geovita_processing_plugin/algorithms/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
"""
44
from .BegrensSkadeExcavation import BegrensSkadeExcavation
55
from .BegrensSkadeImpactMap import BegrensSkadeImpactMap
6-
from .BegrensSkadeTunnel import BegrensSkadeTunnel
6+
from .BegrensSkadeTunnel import BegrensSkadeTunnel
7+
from .CreateAtlasCoverageAlgorithm import CreateAtlasCoverageAlgorithm

geovita_processing_plugin/geovita_processing_plugin_provider.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
from geovita_processing_plugin.algorithms import (
3636
BegrensSkadeExcavation,
3737
BegrensSkadeImpactMap,
38-
BegrensSkadeTunnel
38+
BegrensSkadeTunnel,
39+
CreateAtlasCoverageAlgorithm
3940
)
4041

4142
from geovita_processing_plugin.utilities.gui import GuiUtils
@@ -60,7 +61,7 @@ def loadAlgorithms(self):
6061
"""
6162
Loads all algorithms belonging to this provider.
6263
"""
63-
for alg in [BegrensSkadeExcavation, BegrensSkadeImpactMap, BegrensSkadeTunnel]:
64+
for alg in [BegrensSkadeExcavation, BegrensSkadeImpactMap, BegrensSkadeTunnel, CreateAtlasCoverageAlgorithm]:
6465
self.addAlgorithm(alg())
6566
# add additional algorithms here
6667
# self.addAlgorithm(MyOtherAlgorithm())

0 commit comments

Comments
 (0)