8

I'm digging up a very old question, since the answers don't satisfy me. Part of me thinks "this must be easier today with the load of new, fancy tools we got". The other part of me doesn't understand Python, hence I would love a GUI-based solution.


Premise:

I got a not highly but somewhat irregular line layer which has to be covered by an atlas.

image of a slightly zig-zaggy line, not straight but with a definitive direction, could be a GPS-track

My map item in the layout is a square of about 27.5 cm side length. At a scale of 1:5,000 this gives me a coverage-polygon size of about 1,400 m each side. In order to improve usability of the map I do want the map to be always aligned north, so the polygons mustn't rotate.


Unsastisfactory solutions

  1. I could create a grid covering the whole layer and delete unnecessary grid cells later. This way I have control over margin overlap.

the same zig-zaggy line, covered by a 3 by 5 regular grid of squares, each with a small overlap to its neighbours

My issue with this is: Only six out of 15 grid cells actually cover my layer, and for those six, four don't center around the line.

  1. I could create points along the line, then use the rectangles, ovals, diamonds-tool, or simply the buffer-tool to create the polygons of my coverage layer. (The start of the line is at the right)

the same zig-zaggy line, partly covered by six squares, some with considerable overlap, other with gaps to their neighbours

The problem here is, that some areas aren't covered, while others have a high overlap. I would need to fiddle with the input parameters a lot in order to achieve a satifactory result - or maybe even never achieve it. On the plus side, the line is centered nicely around my line.

  1. I could go ahead, create one coverage polygon of the desired size, and then copy&paste it all along the line - or go ahead from solution #2 and manually improve it.

the same zig-zaggy line, fully covered by squares with small overlap to their neighbours

This is the desired output with few overlaps and no gaps, but I would love for the process to include less manual work. This example is rather simplistic and small. Given longer, more complex features, and/or larger scales, this solution becomes rather time consuming.

  1. I could go all-in, create a square with 1,400 m side length around the start point of the line, convert it to a line layer, intersect that one with my original line, create another square around that point of intersection, convert the second square to a line layer, intersect that one with my original line, drop the first intersection point and second square, create a new square around my second intersection point, etc, pp, until I reach the end of the line. This is very correct - and very time consuming, and absolutely over the top.

What I would like to achieve

I want to be able to create a model, or maybe a short guide, based on native QGIS tools, which with low manual effort provides me with a roughly optimized coverage layer without gaps.

Any ideas on this? Am I asking for something too complexe?

3
  • Provided that the line might come back on its track, and the 3rd square interest the first one, and the 4th one be quite similar to the 2nd. Or is that out of scope of your requirements ? Commented May 22 at 20:27
  • @Kasper for my specific usecase this wont happen; but if it would, I wouldn't need the 3rd and 4th square, since that part of the geometry is already covered by a map window. On the other hand I wont disregard a solution which provides overlapping polygons for switchback-tracks. Commented May 23 at 7:36
  • I won't share as an answer as it comes from an LLM, but I ended up with a working solution with a Virtual Layer. Basically generates squares along the line recursiverly testing how far it needs to go further along the line until the overlap is small enough. Works fine as an atlas. Non fully optimized (squares are centered on the line), but the benefit is that once defined, the VLayer auto-updates. Commented May 24 at 11:05

3 Answers 3

8

It's been a while since I dealt with the same problem and developed a Python solution for it. Your question finally prompted me to develop my very first Python processing algorithm.

Save the following script in a file (i.e. "algRectAlongLines.py"), show QGIS Processing Toolbox and load the file. Then you find the tool in the Scripts folder (=> Generate rectangles along lines).

enter image description here

We can specify the width and height of the rectangles, as well as an overlap in %. By specifying a tolerance value, the source lines can be simplified, which often leads to better results. The rectangles can be aligned to the lines using the Align Rectangles with Line Features check box, which also leads to better results. The newly created polygon layer contains the ID of the associated line, the rotation angle of the rectangle and a field for sorting.

enter image description here

enter image description here

enter image description here

I store all values in global settings variables for further use.

If it turns out that the processing tool is useful, I can also publish it in the QGIS hub.

from qgis.PyQt.QtCore import QVariant
from qgis import processing
from qgis.processing import alg
from qgis.core import (QgsProcessing, QgsGeometry, QgsFeature, QgsWkbTypes, QgsFeatureSink, QgsFields, QgsField, QgsPointXY, QgsSettings, QgsApplication)


@alg(name="algRectAlongLines", label="Generate rectangles along lines", group="", group_label="")
@alg.input(type=alg.SOURCE, name="INPUT", label="Select Line layer", types= [QgsProcessing.TypeVectorLine])
@alg.input(type=alg.NUMBER, name="WIDTH", label="Enter Rectangle Width", default=QgsSettings().value("algRectAlongLines/WIDTH"))
@alg.input(type=alg.NUMBER, name="HEIGHT", label="Enter Rectangle Height", default=QgsSettings().value("algRectAlongLines/HEIGHT"))
@alg.input(type=alg.NUMBER, name="OVERLAP", label="Enter Rectangle Overlap [%]", default=QgsSettings().value("algRectAlongLines/OVERLAP"))
@alg.input(type=alg.NUMBER, name="TOLERANCE", label="Enter Tolerance for Line Simplification", default=QgsSettings().value("algRectAlongLines/TOLERANCE",0))
@alg.input(type=alg.BOOL, name="ALIGN", label="Align Rectangles with Line Features", default=QgsSettings().value("algRectAlongLines/ALIGN",True))
@alg.input(type=alg.SINK, name="OUTPUT", label="Output layer")
def algRectAlongLines(self, parameters, context, feedback, inputs):
    
    def setDefaultValue(dict, param, val):
        s = QgsSettings()
        s.setValue("algRectAlongLines/"+param, val)
        dict[param].setGuiDefaultValueOverride(val)  
        return dict[param]
        
    
    width = self.parameterAsDouble(parameters, 'WIDTH', context)
    height = self.parameterAsDouble(parameters, 'HEIGHT', context)
    overlap = self.parameterAsDouble(parameters, 'OVERLAP', context)
    if overlap > 0:
        overlap = overlap / 100.0
    
    tolerance = self.parameterAsDouble(parameters, 'TOLERANCE', context)
    align = self.parameterAsBool(parameters, 'ALIGN', context)

    sortOrder = None
    i = 1
    
    # refresh the inputs property of the AlgWrapper instance to update the default parameters
    inp = self.inputs
    newInputs = { 'WIDTH': setDefaultValue(inp, 'WIDTH', width),
                  'HEIGHT': setDefaultValue(inp, 'HEIGHT', height),
                  'OVERLAP': setDefaultValue(inp, 'OVERLAP', overlap * 100.0),
                  'TOLERANCE': setDefaultValue(inp, 'TOLERANCE', tolerance),
                  'ALIGN': setDefaultValue(inp, 'ALIGN', align) }
    inp.update(newInputs)
   
    
    source = self.parameterAsSource(parameters, 'INPUT', context)

    fields = QgsFields()
    fields.append(QgsField('src_fid', QVariant.Int))
    fields.append(QgsField('angle', QVariant.Double))
    fields.append(QgsField('sort_order', QVariant.Int))
  

    (sink, dest_id) = self.parameterAsSink(parameters, 'OUTPUT',
                context, fields, QgsWkbTypes.Polygon , source.sourceCrs())

    features = source.getFeatures()
    for feature in features:
        if feature.hasGeometry():
            geom = feature.geometry().extendLine(width*0.1,width*0.1)
            if tolerance != 0:
                geom = geom.simplify(tolerance)
            curs = 0
            numpages = geom.length()/width
            if numpages > 1:
                step = 1.0/numpages
                stepnudge = (1.0-overlap) * step
            else:
                step = 1
                stepnudge = 1
       
            currangle = 0
            while curs < 1:
                startpoint = geom.interpolate(curs*geom.length())
                endpoint = geom.interpolate((curs+step)*geom.length())
                if not endpoint:
                    endpoint = geom.interpolate(geom.length())

                x_start = startpoint.asPoint().x()
                y_start = startpoint.asPoint().y()
                x_end = endpoint.asPoint().x()
                y_end = endpoint.asPoint().y()
                x_mid = (x_start + x_end)/2
                y_mid = (y_start + y_end)/2
          
                w2 = width/2.0
                h2 = height/2.0
                minx = -w2
                miny = -h2
                maxx = w2
                maxy = h2
                poly = QgsGeometry().fromWkt(f'POLYGON(({minx} {miny}, {minx} {maxy},{maxx} {maxy}, {maxx} {miny}, {minx} {miny}))')
            
                feat = QgsFeature()
                feat.setFields(fields)
            
                if align:
                    azimuth = startpoint.asPoint().azimuth(endpoint.asPoint())
                    currangle = (startpoint.asPoint().azimuth(endpoint.asPoint())+270)%360
                    poly.rotate(currangle, QgsPointXY(0,0))
                    feat['angle'] = currangle
                else:
                    feat['angle'] = 0
                
                #poly.translate(-width*overlap,0)
                poly.translate(x_mid, y_mid)
                poly.asPolygon()
                curs = curs + stepnudge
            
                feat['src_fid'] = feature.id()
                feat['sort_order'] = i
                feat.setGeometry(poly)
                sink.addFeature(feat, QgsFeatureSink.FastInsert) 
                i += 1
    
    results = {}
    results['OUTPUT'] = dest_id
    return results
3
  • It is very nice, but I have a question? You enter the height and width of the layout… but where did you set the scale? Otherwise, I don’t understand how it can draw the rectangle at the correct size??? Commented May 25 at 17:23
  • As exemple, if you set height and width for an A4 (210mm x 297mm), it won’t cover the same surface if you are at 1:10’000 or at 1:50’000 Commented May 25 at 17:25
  • 2
    @katagena the tool does not take any scale into account. You have to calculate width and height in drawing units by yourself. But your question is a good hint for an extension. Commented May 25 at 18:02
4

You can use QGIS expressions with Geometry by expression to create such a coverage layer. This can easily be created as a model as well. I created such a model, you can download the model from here. It corresponds to version 2 below, you can select two modes: fixed size of polygons or fixed number of horizontal polygons for each feature.

Download an improved model here: it creates less and better distributed coverage polygons - see under "enhanced" at the end of this solution for details.

Result 1: fixed number of horizontal polygons (here: 10, plus one at the end) - each feature has a different size of coverage polygons; overlap size=20: enter image description here

Result 2: fixed size of polygons, so every feature has the same size, but different (horizontal) number of coverage polygons. Size=400, overlap= 50 on both sides, so each square has a dimension of 500500 m:*

enter image description here

Version 1

The steps to achieve this are (each image of the next screenshot shows the steps):

  1. Create the bounding box around the line.

  2. Create vertical "stripes" that subdivide the bounding box with a freely definable horizontal distance.

  3. Create the intersection of each stripe with the initial line and create the bounding box around this intersection.

  4. Create the centroids of the interesections.

  5. Create a square with pre-defined length, centered around the centroids.

    Edit:

  6. Optional, for lines that are more oriented to the north, there could result some parts of the line uncovered. For this, see the second, revised expression at the bottom and the screenshot with the green coverage layer below.

Steps 1-5: enter image description here

Step 6: enter image description here

Creating smaller squares, for vertical parts of the line there are created a number of vertical squares:

enter image description here

This is the first, easier expression that is based on steps 1-5 from above. Use the more complex expression below insteade if you want to include step 6 as well:

with_variable(
    'no',
    10,  -- define number of horizontal stripes
with_variable(
    'boundingbox',
    bounds(@geometry),
with_variable(
    'size',
    (x_max(@boundingbox)-x_min(@boundingbox))/@no,  -- define length of square
with_variable(
    'overlap',
    80, -- define overlap distance
collect_geometries(
    array_foreach(
        generate_series(0,@no),
with_variable(
    'centr',
    centroid(
        bounds(
            intersection (
                bounds(
                    make_line(
                        make_point (x_min(@boundingbox)+@element*@size,y_min(@boundingbox)),
                        make_point (x_min(@boundingbox)+(@element+1)*@size,y_max(@boundingbox))
                    )
                ),
                @geometry
            )
        )
    ),
    buffer(
        make_regular_polygon(   
            @centr,
            project (@centr, @size/2,0),
            4,
            1
        ),
        @overlap, 
        join:='miter'
    )
)))))))

Version 2

Use this expression instead of the one above for steps 1-6. On line 9, you can define two modes: fixed size or fixed number of polygons:

with_variable(
    'no',
    16,  -- define number of horizontal stripes
with_variable(
    'horsize',
    400,
with_variable(
    'mode', -- define mode: : 0: size depends on horizontal extent of line (0varibale no above) - 1=fixed size (=variable horsize above)
    1,
with_variable(
    'boundingbox',
    bounds(@geometry),
with_variable(
    'size',
    case
        when @mode = 0
        then (x_max(@boundingbox)-x_min(@boundingbox))/@no  -- define length of square
        else @horsize
    end,
with_variable(
    'overlap',
    8, -- define overlap distance
collect_geometries(
    array_filter (
        array_foreach (
            geometries_to_array(
                collect_geometries(
                    array_foreach(
                        generate_series(0, if(@mode=0,@no,(x_max(@boundingbox)-x_min(@boundingbox))/@horsize)),
                        with_variable(
                            'intersec',
                            bounds(
                                intersection (
                                    bounds(
                                        make_line(
                                            make_point (x_min(@boundingbox)+@element*@size,y_min(@boundingbox)),
                                            make_point (x_min(@boundingbox)+(@element+1)*@size,y_max(@boundingbox))
                                        )
                                    ),
                                    @geometry
                                )
                            ),
                            
                                case
                                    when y_max(@intersec)-y_min(@intersec) > @size
                                    then 
                                        collect_geometries(
                                            array_foreach (
                                                generate_series(0, (y_max(@intersec)-y_min(@intersec)) / @size),
                                                centroid(
                                                    bounds (
                                                        make_line(
                                                            make_point (x_min(@intersec), y_min(@intersec)+@element*@size),
                                                            project (
                                                                make_point (x_min(@intersec), y_min(@intersec)+@element*@size),
                                                                @size*sqrt(2),
                                                                radians (45)
                                                            )
                                                        )
                                                    )
                                                )
                                            )
                                        )
                                    else centroid(@intersec)
                                end
                        )
                    )
                )
            ),
            with_variable(
                'result',
                buffer(
                    make_regular_polygon(   
                        @element,
                        project (@element, @size/2,0),
                        4,
                        1
                    ),
                    @overlap, 
                    join:='miter'
                ), 
                case
                    when intersects (@result, @geometry)
                    then @result
                end
            )
        ), 
        @element is not NULL
    )
)))))))

Enhanced version

The 2nd model linked above is an enhanced and improved version, based on the first mode. It creates an optimized output with less and more evenly distributed coverage polygons, which are better centered around the feature to be covered.

See the following image that shows the 4 versions, from left to right:

enter image description here

  1. Result when running the above expression (or using the first model linked above). This creates coverage polygons that are not centered on the line, the line is somewhere on the margins.

  2. The polygons are shifted in a way towards the line so that the line is more in the center of the polygon.

  3. Now, on some sections of the line, there are too many polygons overlapping. When deleting these, you get a coverage with some gaps.

  4. Re-running the initial expression just for these gaps and merging them with the polygons from step 3 gives a nice, optimized coverage with more or less regular polygons that are centered around the line.

To do so:

  1. Shift each polgon's centroid to the centroid of the bounding box that covers the extent of the polygon's interseciton with the line. It is based on this expression:

    translate (
        @geometry,
        x(
            centroid(
                bounds(
                    intersection(
                        @geometry,  
                        overlay_intersects( 
                            'line', 
                            @geometry
                        )[0]
                    )
                )
            )
        )-
        x(
            centroid(@geometry)
        ),
        y(
            centroid(
                bounds(
                    intersection(
                        @geometry,  
                        overlay_intersects( 
                            'line', 
                            @geometry
                        )[0]
                    )
                )
            )
        )-
        y(
            centroid(@geometry)
        )
    )
    
  2. Eliminate (delete) polygons that have too many overlaps. To do so, I create the exclusive geometry of each polygon (the area of each polygon thas has no overlap with any of the other polygons. If the input feature to be covered does not intersect this exclusive area, that means this polygon is unnecessare and can be deleted. The expression to do so:

    case
    when
        intersects (
            difference( 
                @geometry,
                buffer (
                    collect_geometries(
                        array_remove_at(
                            overlay_intersects(
                                @Shift_polygons_OUTPUT, 
                                @geometry, 
                                sort_by_intersection_size:='desc'
                            ),
                            0
                        )
                    ),
                    0
                )
            ),
            overlay_nearest ('line',@geometry)[0]
        )
    then @geometry
    end
    
  3. Repeat creating coverage polygons for the parts of the line that remains uncovered after step 3: on the following screenshot the red portions of the initial blue line. For these red segments, the initial process to create coverage polygons is repeated and the output merged with the one of step 3.

enter image description here

3
  • 2
    This is a very nice idea, Babel, but wouldn't this approach fail for lines which are mainly oriented north to south? Maybe one would have to check, which side of the bounding box is longer and then divide that side into stripes? Commented May 26 at 8:28
  • They are all « north oriented », in the exemple above, you can see that the rectangle follows the line Commented May 26 at 14:02
  • You're right, @Erik, I added a version of the expression that takes this into account Commented May 26 at 16:27
2

On the base of @christoph script, I adapt it to take in account the height and width of the layout, and I add the scale:

from qgis.PyQt.QtCore import QVariant
from qgis import processing
from qgis.processing import alg
from qgis.core import QgsProcessing, QgsGeometry, QgsFeature, QgsWkbTypes, QgsFeatureSink, QgsFields, QgsField, QgsPointXY, QgsSettings


@alg(name="algRectAlongLines", label="Generate rectangles along lines", group="Drosera", group_label="drosera")
@alg.input(type=alg.SOURCE, name="INPUT", label="Select Line layer", types=[QgsProcessing.TypeVectorLine])
@alg.input(type=alg.NUMBER, name="WIDTH", label="Enter Layout Rectangle Width (mm)", default=QgsSettings().value("algRectAlongLines/width", 10))
@alg.input(type=alg.NUMBER, name="HEIGHT", label="Enter Layout Rectangle Height (mm)", default=QgsSettings().value("algRectAlongLines/height", 10))
@alg.input(type=alg.NUMBER, name="SCALE", label="Enter Layout Map Scale (e.g. 1000 for 1:1000)", default=QgsSettings().value("algRectAlongLines/scale", 1000))
@alg.input(type=alg.NUMBER, name="OVERLAP", label="Enter Rectangle Overlap [%]", default=QgsSettings().value("algRectAlongLines/overlap", 0))
@alg.input(type=alg.NUMBER, name="TOLERANCE", label="Enter Tolerance for Line Simplification", default=QgsSettings().value("algRectAlongLines/tolerance", 0))
@alg.input(type=alg.BOOL, name="ALIGN", label="Align Rectangles with Line Features", default=QgsSettings().value("algRectAlongLines/align", True))
@alg.input(type=alg.SINK, name="OUTPUT", label="Output layer")

def algRectAlongLines(self, parameters, context, feedback, inputs):
    """
    """
    s = QgsSettings()

    width_mm = self.parameterAsDouble(parameters, 'WIDTH', context)
    height_mm = self.parameterAsDouble(parameters, 'HEIGHT', context)
    scale = self.parameterAsDouble(parameters, 'SCALE', context)

    if width_mm != s.value("algRectAlongLines/width"):
        s.setValue("algRectAlongLines/width", width_mm)
    if height_mm != s.value("algRectAlongLines/height"):
        s.setValue("algRectAlongLines/height", height_mm)
    if scale != s.value("algRectAlongLines/scale"):
        s.setValue("algRectAlongLines/scale", scale)

    width = (width_mm / 1000.0) * scale
    height = (height_mm / 1000.0) * scale

    overlap = self.parameterAsDouble(parameters, 'OVERLAP', context)
    if overlap != s.value("algRectAlongLines/overlap"):
        s.setValue("algRectAlongLines/overlap", overlap)
    if overlap > 0:
        overlap = overlap / 100.0

    tolerance = self.parameterAsDouble(parameters, 'TOLERANCE', context)
    if tolerance != s.value("algRectAlongLines/tolerance"):
        s.setValue("algRectAlongLines/tolerance", tolerance)

    align = self.parameterAsBool(parameters, 'ALIGN', context)
    if align != s.value("algRectAlongLines/align"):
        s.setValue("algRectAlongLines/align", align)

    i = 1
    source = self.parameterAsSource(parameters, 'INPUT', context)

    fields = QgsFields()
    fields.append(QgsField('src_fid', QVariant.Int))
    fields.append(QgsField('angle', QVariant.Double))
    fields.append(QgsField('sort_order', QVariant.Int))

    (sink, dest_id) = self.parameterAsSink(parameters, 'OUTPUT',
                context, fields, QgsWkbTypes.Polygon , source.sourceCrs())

    features = source.getFeatures()
    for feature in features:
        geom = feature.geometry()
        if tolerance != 0:
            geom = geom.simplify(tolerance)
        curs = 0
        numpages = geom.length() / width
        step = 1.0 / numpages
        stepnudge = (1.0 - overlap) * step

        currangle = 0
        while curs < 1:
            startpoint = geom.interpolate(curs * geom.length())
            endpoint = geom.interpolate((curs + step) * geom.length())
            if not endpoint:
                endpoint = geom.interpolate(geom.length())

            x_start = startpoint.asPoint().x()
            y_start = startpoint.asPoint().y()
            x_end = endpoint.asPoint().x()
            y_end = endpoint.asPoint().y()
            x_mid = (x_start + x_end) / 2
            y_mid = (y_start + y_end) / 2

            w2 = width / 2.0
            h2 = height / 2.0
            minx = -w2
            miny = -h2
            maxx = w2
            maxy = h2
            poly = QgsGeometry().fromWkt(
                f'POLYGON(({minx} {miny}, {minx} {maxy},{maxx} {maxy}, {maxx} {miny}, {minx} {miny}))'
            )

            feat = QgsFeature()
            feat.setFields(fields)

            if align:
                azimuth = startpoint.asPoint().azimuth(endpoint.asPoint())
                currangle = (azimuth + 270) % 360
                poly.rotate(currangle, QgsPointXY(0, 0))
                feat['angle'] = currangle
            else:
                feat['angle'] = 0

            poly.translate(-width * overlap, 0)
            poly.translate(x_mid, y_mid)
            poly.asPolygon()
            curs = curs + stepnudge

            feat['src_fid'] = feature.id()
            feat['sort_order'] = i
            feat.setGeometry(poly)
            sink.addFeature(feat, QgsFeatureSink.FastInsert)
            i += 1

    results = {}
    results['OUTPUT'] = dest_id
    return results

But I noticed a problem when the line has "sharpe" angle, cause the start and end point are "centered". So if the line goes "outside" the rectangle, the whole line is not taking in account. enter image description here

So I adapt it to calculate the azimut at the midpoint, and not between the start and endpoint:

from qgis.PyQt.QtCore import QVariant
from qgis import processing
from qgis.processing import alg
from qgis.core import (
    QgsProcessing,
    QgsGeometry,
    QgsFeature,
    QgsWkbTypes,
    QgsFeatureSink,
    QgsFields,
    QgsField,
    QgsPointXY,
    QgsSettings
)

@alg(name="algRectAlongLines", label="Generate rectangles along lines", group="Drosera", group_label="drosera")
@alg.input(type=alg.SOURCE, name="INPUT", label="Select Line layer", types=[QgsProcessing.TypeVectorLine])
@alg.input(type=alg.NUMBER, name="WIDTH", label="Enter Layout Rectangle Width (mm)", default=QgsSettings().value("algRectAlongLines/width", 10))
@alg.input(type=alg.NUMBER, name="HEIGHT", label="Enter Layout Rectangle Height (mm)", default=QgsSettings().value("algRectAlongLines/height", 10))
@alg.input(type=alg.NUMBER, name="SCALE", label="Enter Layout Map Scale (e.g. 1000 for 1:1000)", default=QgsSettings().value("algRectAlongLines/scale", 1000))
@alg.input(type=alg.NUMBER, name="OVERLAP", label="Enter Rectangle Overlap [%]", default=QgsSettings().value("algRectAlongLines/overlap", 0))
@alg.input(type=alg.NUMBER, name="TOLERANCE", label="Enter Tolerance for Line Simplification", default=QgsSettings().value("algRectAlongLines/tolerance", 0))
@alg.input(type=alg.BOOL, name="ALIGN", label="Align Rectangles with Line Features", default=QgsSettings().value("algRectAlongLines/align", True))
@alg.input(type=alg.SINK, name="OUTPUT", label="Output layer")
def algRectAlongLines(self, parameters, context, feedback, inputs):
    """
    """
    s = QgsSettings()

    width_mm = self.parameterAsDouble(parameters, 'WIDTH', context)
    height_mm = self.parameterAsDouble(parameters, 'HEIGHT', context)
    scale = self.parameterAsDouble(parameters, 'SCALE', context)

    if width_mm != s.value("algRectAlongLines/width"):
        s.setValue("algRectAlongLines/width", width_mm)
    if height_mm != s.value("algRectAlongLines/height"):
        s.setValue("algRectAlongLines/height", height_mm)
    if scale != s.value("algRectAlongLines/scale"):
        s.setValue("algRectAlongLines/scale", scale)

    width = (width_mm / 1000.0) * scale
    height = (height_mm / 1000.0) * scale

    overlap = self.parameterAsDouble(parameters, 'OVERLAP', context)
    if overlap != s.value("algRectAlongLines/overlap"):
        s.setValue("algRectAlongLines/overlap", overlap)
    if overlap > 0:
        overlap = overlap / 100.0

    tolerance = self.parameterAsDouble(parameters, 'TOLERANCE', context)
    if tolerance != s.value("algRectAlongLines/tolerance"):
        s.setValue("algRectAlongLines/tolerance", tolerance)

    align = self.parameterAsBool(parameters, 'ALIGN', context)
    if align != s.value("algRectAlongLines/align"):
        s.setValue("algRectAlongLines/align", align)

    source = self.parameterAsSource(parameters, 'INPUT', context)
    fields = QgsFields()
    fields.append(QgsField('src_fid', QVariant.Int))
    fields.append(QgsField('angle', QVariant.Double))
    fields.append(QgsField('sort_order', QVariant.Int))

    (sink, dest_id) = self.parameterAsSink(parameters, 'OUTPUT', context, fields, QgsWkbTypes.Polygon, source.sourceCrs())

    i = 1
    features = source.getFeatures()
    for feature in features:
        geom = feature.geometry()
        if tolerance != 0:
            geom = geom.simplify(tolerance)
        geom_length = geom.length()
        curs = 0
        numpages = geom_length / width
        if numpages == 0:
            continue
        step = 1.0 / numpages
        stepnudge = (1.0 - overlap) * step

        while curs < 1:
            startpoint = geom.interpolate(curs * geom_length)
            endpoint = geom.interpolate((curs + step) * geom_length)
            if not endpoint:
                endpoint = geom.interpolate(geom_length)

            x_start = startpoint.asPoint().x()
            y_start = startpoint.asPoint().y()
            x_end = endpoint.asPoint().x()
            y_end = endpoint.asPoint().y()
            x_mid = (x_start + x_end) / 2
            y_mid = (y_start + y_end) / 2

            w2 = width / 2.0
            h2 = height / 2.0
            minx = -w2
            miny = -h2
            maxx = w2
            maxy = h2
            poly = QgsGeometry().fromWkt(
                f'POLYGON(({minx} {miny}, {minx} {maxy},{maxx} {maxy}, {maxx} {miny}, {minx} {miny}))'
            )

            feat = QgsFeature()
            feat.setFields(fields)

            if align:
                center_dist = (curs + step * 0.5) * geom_length
                epsilon = geom_length * 0.001
                p1 = geom.interpolate(max(center_dist - epsilon, 0)).asPoint()
                p2 = geom.interpolate(min(center_dist + epsilon, geom_length)).asPoint()
                azimuth = p1.azimuth(p2)
                currangle = (azimuth + 270) % 360
                poly.rotate(currangle, QgsPointXY(0, 0))
                feat['angle'] = currangle
            else:
                feat['angle'] = 0

            poly.translate(-width * overlap, 0)
            poly.translate(x_mid, y_mid)
            curs = curs + stepnudge

            feat['src_fid'] = feature.id()
            feat['sort_order'] = i
            feat.setGeometry(poly)
            sink.addFeature(feat, QgsFeatureSink.FastInsert)
            i += 1

    results = {}
    results['OUTPUT'] = dest_id
    return results

It works: enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.