5

I want to flip a vector polygon about an arbitrary axis. What I would like to do is select the polygon to flip, pick two points (A & B in image below) identifying the axis about which the flip will occur, and get a result that looks like the red-dashed polygon.

What I have tried: searching for 'Flip', 'Mirror', 'Transform' in QGIS processing tools and web searches.

As a workaround, I've been using the 'Shape Tools' plugin 'Flip Horiz/Vert' in combination with QGIS Vector Tools 'Rotate'.

vector flip about arbitrary line

1
  • Phew... I guess you need to create a line from your points, then extend it, then calculate the distance and direction of each polygon vertex to the line, then translate each vertex by that distance, and finally convert your vertices back to a line and that to a polygon. I doubt this is possible without creating a new layer.
    – Erik
    Jun 8 at 13:45

2 Answers 2

5

I wrote this code that reflects a list of points between a line created by two points. I don't know the best way for you to input the feature and the points, so the implementation is very basic and you'll probably need to fix some lines for your main purpose.

Note: I am not an expert mathematician nor a python programmer. Maybe this script is inefficient but the important thing is to get the idea.

  1. Define function to reflect a list of QgsPointXY between a line defined by two points.

    def reflectPoints(input_points, refl_pt_a, refl_pt_b):
        try:
            m = (refl_pt_a.y() - refl_pt_b.y()) / (refl_pt_a.x() - refl_pt_b.x()) # get the slope of the reflection line
            b = refl_pt_a.y() - m * refl_pt_a.x() # get how 'shifted' the line is
            try:
                inv_m = -1 / m # get the slope of perpendicular line to the reflection line
                for x, y in input_points: # loop throw the list of points
                    # create a perpendicular line that touches the n point of input_points
                    b_n = y - inv_m * x # find how 'shifted' the perpendicular line is
                    l_x_coord = (b_n - b) / (m - inv_m) # get the x coordinate of the intersection between the reflection line and the perpendicular line
                    l_y_coord = m * l_x_coord + b # get the y coordinate of the intersection between the reflection line and the perpendicular line
                    new_x = 2 * l_x_coord - x # different approach of the Midpoint theorem to find the x coordinate reflected
                    new_y = 2 * l_y_coord - y # different approach of the Midpoint theorem to find the y coordinate reflected
                    yield QgsPointXY(new_x, new_y)
            except ZeroDivisionError: # handle reflection across horizontal line (undefined slope)
                for x, y in input_points:
                    l_y_coord = refl_pt_a.y()
                    new_x = x
                    new_y = 2 * l_y_coord - y
                    yield QgsPointXY(new_x, new_y)
        except ZeroDivisionError: # handle reflection across vertical line (no slope)
            for x, y in input_points:
                l_x_coord = refl_pt_a.x()
                new_x = 2 * l_x_coord - x
                new_y = y
                yield QgsPointXY(new_x, new_y)
    
    
  2. Get the points to build the reflection line, I will define this function to capture the mouse when the map canvas is clicked.

    canva = iface.mapCanvas()
    map_tool = QgsMapToolEmitPoint(canva)
    canva.setMapTool(map_tool)
    old_point = False # this variable will store the first point of the reflection line
    def getPoints(point, button):
        global old_point
        if not old_point and button == 1:
            old_point = QgsPointXY(point)
        elif old_point and button == 2:
            input_layer = iface.activeLayer() # get active layer
            feature = next(input_layer.getSelectedFeatures()) # get the selected layer, here it only takes one feature if more than one where selected
            input_points = feature.geometry().asPolygon()[0] # get the geometry of only one polygon if multipolygon geometry
            output_points = list(reflectPoints(input_points, point, old_point)) # reflect the vertices
            input_layer.startEditing()
            geometry = QgsGeometry.fromPolygonXY([output_points])
            input_layer.changeGeometry(feature.id(), geometry)
            old_point = False
    
    map_tool.canvasClicked.connect(getPoints)
    

How to use it

Paste and run the shown scripts as a new python script in the QGIS python console, then select the feature to reflect, select with left click the first point of the reflection line and select the second point with right click and the feature will be reflected. Execute this line in the python console map_tool.canvasClicked.disconnect(getPoints) for disabling the reflection tool.

Notes

This script is very basic, so it works only with single part polygon geometries.

This code modifies and overrides the features geometries, so be careful

This doesn't include snapping tools.

Edit

Snapping enabled. You only need to paste the code in the QGIS python console, then select the feature you want to reflect, run the code, left click first point of the reflection line right click second point of the reflection line.

def reflectPoints(input_points, refl_pt_a, refl_pt_b):
    try:
        m = (refl_pt_a.y() - refl_pt_b.y()) / (refl_pt_a.x() - refl_pt_b.x()) # get the slope of the reflection line
        b = refl_pt_a.y() - m * refl_pt_a.x() # get how 'shifted' the line is
        try:
            inv_m = -1 / m # get the slope of perpendicular line to the reflection line
            for x, y in input_points: # loop throw the list of points
                # create a perpendicular line that touches the n point of input_points
                b_n = y - inv_m * x # find how 'shifted' the perpendicular line is
                l_x_coord = (b_n - b) / (m - inv_m) # get the x coordinate of the intersection between the reflection line and the perpendicular line
                l_y_coord = m * l_x_coord + b # get the y coordinate of the intersection between the reflection line and the perpendicular line
                new_x = 2 * l_x_coord - x # different approach of the Midpoint theorem to find the x coordinate reflected
                new_y = 2 * l_y_coord - y # different approach of the Midpoint theorem to find the y coordinate reflected
                yield QgsPointXY(new_x, new_y)
        except ZeroDivisionError: # handle reflection across horizontal line (undefined slope)
            for x, y in input_points:
                l_y_coord = refl_pt_a.y()
                new_x = x
                new_y = 2 * l_y_coord - y
                yield QgsPointXY(new_x, new_y)
    except ZeroDivisionError: # handle reflection across vertical line (no slope)
        for x, y in input_points:
            l_x_coord = refl_pt_a.x()
            new_x = 2 * l_x_coord - x
            new_y = y
            yield QgsPointXY(new_x, new_y)
canvas = iface.mapCanvas()
map_tool = QgsMapToolCapture(canvas, iface.cadDockWidget(), QgsMapToolCapture.CaptureLine)
map_tool.setAutoSnapEnabled(True)
map_tool.startCapturing()
canvas.setMapTool(map_tool)
old_point = False # this variable will store the first point of the reflection line
def getPoints(mouse):
    point = mouse.mapPoint()
    button = mouse.button()
    global old_point
    if not old_point and button == 1:
        old_point = QgsPointXY(point)
    elif old_point and button == 2:
        input_layer = iface.activeLayer() # get active layer
        feature = next(input_layer.getSelectedFeatures()) # get the selected layer, here it only takes one feature if more than one where selected
        input_points = feature.geometry().asPolygon()[0] # get the geometry of only one polygon if multipolygon geometry
        output_points = list(reflectPoints(input_points, point, old_point)) # reflect the vertices
        input_layer.startEditing()
        geometry = QgsGeometry.fromPolygonXY([output_points])
        input_layer.changeGeometry(feature.id(), geometry)
        old_point = False
        input_layer.commitChanges()
        global map_tool
        global canvas
        canvas.unsetMapTool(map_tool)

map_tool.cadCanvasPressEvent = getPoints
0
3

You can use the "Geometry Generator" to dynamically create a copy (visualization only) of your polygon, flipped around the line on the layer axis using the following expression. Whenever you change the line, the flipped polygon will adapt immediately:

make_polygon(
    make_line (
        array_foreach (
            generate_series (1,  num_points( $geometry)),
            with_variable (
                'line',
                make_line (
                    point_n( $geometry, @element),
                    closest_point( 
                        extend (
                            overlay_nearest(
                                'axis',  --change the name to the name of the line layer you use
                                $geometry
                            )[0], 
                            100000, -- adapt these distances on this and the next line (just needed in case the axis is shorter then the polygon)
                            100000
                        ),
                        point_n ($geometry, @element)
                    )
                ),
                end_point (
                    extend (@line,0,length(@line))
                )
            )
        )
    )
)

Red: initial polygon, blue: flipped polygon; black: axis enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.