I'm trying to label the polygon boundaries instead of an anchor point. Not sure if this is possible. For this example I wish to label these numbers in the top left corner of every polygon.
6 Answers
-
Tried your method but since my polygons are shaped in different ways it will not work like I want it to. Thanks anyway! Nov 4, 2016 at 13:14
-
1Bit complicated but i will try to work this method out! Thanks Nov 15, 2016 at 12:45
I know you're looking for QGIS answer, but I think you could tailor the method below to suit your needs.
Basically:
- Loop through the vertices in each polygon
- Assign a score for each vertex, considering the distance from the polygon centroid to the vertex, and the angle from the centroid to the vertex. You can weight the components if you want to consider distance or angle more heavily (e.g. below, I only weight distance by half). My method below calculates a score by giving positive points for distance and subtracting a penalty for being far away from 45 degrees (NW). I think it's correct, but you should check the math.
- Write the highest scoring points to a new feature class
- Use those points to label
'
fc = 'Parcels'
sr = arcpy.Describe(fc).spatialReference
points = []
with arcpy.da.SearchCursor(fc,'SHAPE@',spatial_reference=sr) as cursor:
for row in cursor:
score = 0
for part in row[0]:
for pnt in part:
pnt_geom = arcpy.PointGeometry(pnt)
dx = row[0].centroid.X - pnt.X
dy = row[0].centroid.Y - pnt.Y
ang = math.degrees(math.atan2(dy,dx))
cur_score = (pnt_geom.distanceTo(row[0].centroid)*0.5) - (pnt_geom.distanceTo(row[0].centroid)*0.5 * abs(ang+45)/180)
if cur_score > score:
score = cur_score
point = pnt_geom
points.append(point)
arcpy.CopyFeatures_management(points,r'in_memory\points')
-
-
whilst its nice code, the output is a seperate feature class, which would then have to be managed and recreated/updated if the underlying source data changes. The Arcpy libraries imply an ESRI solution, when the OP is asking about QGIS. A better response is below by 'Hydrographer' which uses OOTB QGIS functionality and doesn't create additional feature classes.– nr_ausApr 21, 2021 at 2:02
I'm not sure that is really possible with such randomly shaped polygons. I think you best bet would be to extract the centroids using 'Vector > Geometry Tools > Polygon centroids'. Then move each centroid to the appropriate location and label up this layer instead.
-
Moving the centroids didnt work out for me. Will try some more and then might get a result. Thanks! Nov 15, 2016 at 12:45
I have the process in theory however what you are asking is not feasible always. It should be decided which is more important, to being top point or the most left? (please see attached picture)
Sometimes can have just one point, but sometimes we need compromise. My idea will work in some cases and not in some.
So the idea is to export all the nodes into csv. vector-geometry tools-extract nodes. save into shape. save as csv.
In excel you need to find 2 points in each group of points (one group of points is one polygon - use ID for separation), the point with the smallest X and the point with the smallest Y. If both the same point than you good, if not I would go with the mid point . At the end you will have a point csv with all the mid or top/left points. Load back the point into qgis creating points and activate the label.
https://stackoverflow.com/questions/17114443/excel-how-to-get-min-value-in-a-group
This question has good answers already, but I thought I'd provide a dynamic solution for anyone who has this problem specifically with polygons that are only 4-sided (for example, rectangles showing the extents of lots of smaller maps over a larger map).
This solution involves writing several python functions in the Function Editor. I am sure there are ways to make my code more concise/elegant - I've only been working with QGIS for 8 months, and I still have a ton to learn about the Python API.
System Info:
- QGIS 3.16.0 Hannover
- Windows 10 Home
X-Coordinate
I'm going to write a function that determines the top left point, and then a second function that determines that point's x-coordinate. I split this part into two functions because the first is useful in lots of different situations.
In your vector layer properties, go to Labels > Placement > Data defined, click on Coordinate X and select Edit. Go to the Function Editor tab and insert the following code, saving as top-left-point
:
from qgis.core import *
from qgis.gui import *
@qgsfunction(args='auto', group='Custom')
def top_left_point(feature, parent):
"""
Returns the point in the top-left of the given feature's perimeter
"""
def extent_point_in_position(horz, vert, feature):
"""
Returns the point whose coordinates correspond to the given vertical and horizontal descriptions
"""
# Convert geometry into a list of points in simple [x,y] form
geom = feature.geometry().asPolygon()
extents = [[point.x(),point.y()] for point in geom[0]] #unwrap the QgsPoint objects (unclear why geom is several nested lists)
# Sort points into ascending order of x values
extents.sort(key=lambda point: point[0])
if horz == "left":
left_points = [extents[0], extents[1]] #the two points with the lowest x values
left_points.sort(key=lambda point: point[1]) #order points by ascending y values
if vert == "bottom":
return QgsPointXY(left_points[0][0],left_points[0][1]) #re-wrap points into QgsPoint object
elif vert == "top":
return QgsPointXY(left_points[1][0],left_points[1][1])
elif horz == "right":
right_points = [extents[-2], extents[-1]] #the two points with the highest x values
right_points.sort(key=lambda point: point[1]) #order points by ascending y values
if vert == "bottom":
return QgsPointXY(right_points[0][0],right_points[0][1])
elif vert == "top":
return QgsPointXY(right_points[1][0],right_points[1][1])
# Explicitly set vert and horz
vert = "top"
horz = "left"
# Determine the point
return extent_point_in_position(horz, vert, feature)
This function returns a QgsPoint object. We need to get the x value of the point. To do this, add another function pointx()
in the editor:
from qgis.core import *
from qgis.gui import *
@qgsfunction(args='auto', group='Custom')
def pointx(point, feature, parent):
"""
Returns the x value of a QgsPoint object
"""
return point.x()
Switch back to the Expression tab. Both functions should be present under the Custom group. Write the expression pointx(top_left_point())
and click OK.
Y-Coordinate
As with the x-coordinate, click on Y under Data defined and select Edit. Go to the Function Editor tab and add the function pointy()
:
from qgis.core import *
from qgis.gui import *
@qgsfunction(args='auto', group='Custom')
def pointy(point, feature, parent):
"""
Returns the y value of a QgsPoint object
"""
return point.y()
Now return to the Expression tab and write the expression pointy(top_left_point())
and click OK.
The labels should be located next to the top-left point of the extents.
Adding Rotation
If you want to rotate the labels to match the angle of the side of the polygon that they're adjacent to, click on Rotation under Data defined, and bring up the Function Editor again. Add the following function as top-left-rotation
:
from qgis.core import *
from qgis.gui import *
import math
@qgsfunction(args='auto', group='Custom')
def top_left_rotation(feature, parent):
"""
Calculates the angle of the top-left line of a rectangular polygon
"""
def extent_point_in_position(horz, vert, feature):
"""
Returns the point whose coordinates correspond to the given vertical and horizontal descriptions
"""
# Convert geometry into a list of points in simple [x,y] form
geom = feature.geometry().asPolygon()
extents = [[point.x(),point.y()] for point in geom[0]] #unwrap the QgsPoint objects (unclear why geom is several nested lists)
# Sort points into ascending order of x values
extents.sort(key=lambda point: point[0]) #on lambda keyword, see https://dbader.org/blog/python-min-max-and-nested-lists#fn:2
if horz == "left":
left_points = [extents[0], extents[1]] #the two points with the lowest x values
left_points.sort(key=lambda point: point[1]) #order points by ascending y values
if vert == "bottom":
return left_points[0]
elif vert == "top":
return left_points[1]
elif horz == "right":
right_points = [extents[-2], extents[-1]] #the two points with the highest x values
right_points.sort(key=lambda point: point[1]) #order points by ascending y values
if vert == "bottom":
return right_points[0]
elif vert == "top":
return right_points[1]
# Get top left and right points
p1 = extent_point_in_position("left", "top", feature)
p2 = extent_point_in_position("right", "top", feature)
# Find angle
angle = (-1) * math.atan((p2[1] - p1[1]) / (p2[0] - p1[0]))
return math.degrees(angle)
Return to the Expression tab and write the expression top_left_rotation()
, then click OK. Your labels should now be rotated to match the orientations of the top left sides of your polygons.
Edit (20 April 2021)
While it might appear that my functions for the x and y coordinates basically replicate pigreco's answer, they don't: x(point_n($geometry,1))
and y(point_n($geometry,1))
will give incorrect results when the geometry is drawn by hand, because the point at index 1 will not necessarily be in the top-left.