[{"data":1,"prerenderedAt":2936},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates":3},{"id":4,"title":5,"body":6,"description":2925,"extension":2926,"meta":2927,"navigation":313,"path":2932,"seo":2933,"stem":2934,"__hash__":2935},"docs\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Findex.md","Geometry Operations and Spatial Predicates in PyQGIS",{"type":7,"value":8,"toc":2910},"minimark",[9,13,22,40,208,213,253,257,265,281,405,426,436,488,496,500,506,607,733,750,769,773,805,974,1007,1169,1188,1192,1217,1330,1544,1564,1569,1632,1647,1651,1657,1822,1856,1860,1873,1880,2008,2042,2046,2053,2226,2376,2408,2412,2423,2532,2558,2562,2568,2680,2708,2719,2722,2726,2789,2793,2806,2817,2825,2847,2853,2857,2906],[10,11,5],"h1",{"id":12},"geometry-operations-and-spatial-predicates-in-pyqgis",[14,15,16,17,21],"p",{},"Every Processing algorithm that clips, buffers, dissolves, or joins is ultimately a loop over ",[18,19,20],"code",{},"QgsGeometry"," calls. Working one level down — directly on the geometry objects — is what you do when the algorithm you need does not exist, when you want to avoid writing intermediate layers to disk, or when a per-feature decision has to be made in Python rather than expressed as a parameter dictionary.",[14,23,24,25,30,31,35,36,39],{},"This guide sits inside ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation"," and complements the algorithm-driven pages around it. Where ",[26,32,34],{"href":33},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002F","Vector Data Manipulation"," shows you how to call ",[18,37,38],{},"native:clip"," on a whole layer, this one explains what the clip is doing to each geometry, why it sometimes fails, and how to perform the same operation yourself when the layer-level tool is the wrong granularity. It covers the geometry object model, the constructive operations, the spatial predicates and their notorious edge cases, correct distance and area measurement, spatial indexing, and geometry validity.",[14,41,42],{},[43,44,49,53,57,64,81,90,100,106,111,117,122,126,130,134,138,141,144,151,155,159,165,171,175,179,182,185,190,193,196,200,205],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 292","img","The PyQGIS geometry object model: a feature holds a QgsGeometry, which wraps a concrete abstract geometry such as a point, line string or polygon, each of which has a multi-part counterpart","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"What a QgsGeometry contains",[54,55,56],"desc",{},"A QgsFeature holds one QgsGeometry. The QgsGeometry is a lightweight handle wrapping a QgsAbstractGeometry, which is concretely a QgsPoint, QgsLineString, QgsPolygon or one of their multi-part counterparts. The handle carries the WKB type and validity state while the wrapped geometry carries the coordinates.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","292","#f6f3ea",[65,66,67],"defs",{},[68,69,76],"marker",{"id":70,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"geoModelArrow","0 0 10 10","8","5","7","auto-start-reverse",[77,78],"path",{"d":79,"fill":80},"M0 0 L10 5 L0 10 z","#2f3b35",[82,83,89],"text",{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A handle, a wrapped geometry, and the concrete types beneath it",[58,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":99},"24","52","150","70","10","#fffdf7","#2563eb","stroke-width:2.5",[82,101,105],{"x":102,"y":103,"style":104,"fill":87,"textAnchor":88},"99","80","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","QgsFeature",[82,107,110],{"x":102,"y":108,"style":109,"fill":80,"textAnchor":88},"102","text-anchor:middle;font-size:11.5px;font-family:sans-serif","attributes + one geometry",[58,112],{"x":113,"y":93,"width":114,"height":95,"rx":96,"fill":115,"stroke":116,"style":99},"216","180","#26322d","#0f766e",[82,118,20],{"x":119,"y":120,"style":104,"fill":121,"textAnchor":88},"306","78","#d9f99d",[82,123,125],{"x":119,"y":124,"style":109,"fill":121,"textAnchor":88},"98","value-type handle",[82,127,129],{"x":119,"y":128,"style":109,"fill":121,"textAnchor":88},"114","wkbType() · isValid()",[58,131],{"x":132,"y":93,"width":114,"height":95,"rx":96,"fill":97,"stroke":133,"style":99},"438","#b45309",[82,135,137],{"x":136,"y":120,"style":104,"fill":87,"textAnchor":88},"528","QgsAbstractGeometry",[82,139,140],{"x":136,"y":124,"style":109,"fill":80,"textAnchor":88},"holds the coordinates",[82,142,143],{"x":136,"y":128,"style":109,"fill":133,"textAnchor":88},"constGet()",[145,146],"line",{"x1":147,"y1":148,"x2":149,"y2":148,"stroke":80,"style":150},"174","87","212","stroke-width:2.5;marker-end:url(#geoModelArrow)",[145,152],{"x1":153,"y1":148,"x2":154,"y2":148,"stroke":80,"style":150},"396","434",[145,156],{"x1":136,"y1":157,"x2":136,"y2":158,"stroke":133,"style":150},"122","152",[58,160],{"x":92,"y":161,"width":113,"height":162,"rx":96,"fill":97,"stroke":163,"style":164},"164","106","#15803d","stroke-width:2",[82,166,170],{"x":167,"y":168,"style":169,"fill":87,"textAnchor":88},"132","188","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","Point",[172,173],"circle",{"cx":167,"cy":174,"r":74,"fill":163},"222",[82,176,178],{"x":167,"y":177,"style":109,"fill":80,"textAnchor":88},"256","QgsPoint · QgsMultiPoint",[58,180],{"x":181,"y":161,"width":113,"height":162,"rx":96,"fill":97,"stroke":163,"style":164},"272",[82,183,184],{"x":84,"y":168,"style":169,"fill":87,"textAnchor":88},"Line",[77,186],{"d":187,"fill":188,"stroke":163,"style":189},"M312 236 L346 208 L382 232 L414 204 L448 226","none","stroke-width:3;stroke-linejoin:round",[82,191,192],{"x":84,"y":177,"style":109,"fill":80,"textAnchor":88},"QgsLineString · QgsMultiLineString",[58,194],{"x":195,"y":161,"width":113,"height":162,"rx":96,"fill":97,"stroke":163,"style":164},"520",[82,197,199],{"x":198,"y":168,"style":169,"fill":87,"textAnchor":88},"628","Polygon",[77,201],{"d":202,"fill":163,"fillOpacity":203,"stroke":163,"style":204},"M584 236 L604 202 L648 200 L672 228 L644 244 Z",0.25,"stroke-width:2.5;stroke-linejoin:round",[82,206,207],{"x":198,"y":177,"style":109,"fill":80,"textAnchor":88},"QgsPolygon · QgsMultiPolygon",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,227,234,246],"ul",{},[217,218,219,223,224,226],"li",{},[220,221,222],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer. ",[18,225,20],{}," has been API-stable across 3.x; the few method renames are noted inline.",[217,228,229,230,233],{},"A vector layer loaded in the project, or a path you can open with ",[18,231,232],{},"QgsVectorLayer",".",[217,235,236,237,240,241,245],{},"A ",[220,238,239],{},"projected CRS"," for anything involving distance or area. If your data is in EPSG:4326, read ",[26,242,244],{"href":243},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS"," first — measuring in degrees is the most common source of wrong answers on this page.",[217,247,248,249,233],{},"Comfort with the Python Console; see ",[26,250,252],{"href":251},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002F","QGIS Python Console Basics",[209,254,256],{"id":255},"what-a-qgsgeometry-actually-holds","What a QgsGeometry actually holds",[14,258,259,261,262,264],{},[18,260,20],{}," is a thin, copy-on-write handle. The coordinates live in a ",[18,263,137],{}," subclass underneath, and the handle carries the WKB type plus a cached validity flag. That indirection matters in practice for two reasons.",[14,266,267,268,273,274,277,278,280],{},"First, ",[220,269,270,272],{},[18,271,20],{}," behaves like a value",", not a reference. Assigning one to another copies it lazily, so mutating a geometry you pulled off a feature does not silently edit the layer. Second, ",[220,275,276],{},"the concrete type is reachable but usually unnecessary",": ",[18,279,143],{}," returns the wrapped abstract geometry when you need vertex-level access, but the convenience methods on the handle cover most work.",[282,283,288],"pre",{"className":284,"code":285,"language":286,"meta":287,"style":287},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsWkbTypes\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nfeature = next(layer.getFeatures())\ngeometry = feature.geometry()\n\nprint(QgsWkbTypes.displayString(geometry.wkbType()))  # e.g. \"Polygon\"\nprint(geometry.isMultipart())                          # False for a single polygon\nprint(geometry.constGet().vertexCount())               # coordinate count\n","python","",[18,289,290,308,315,340,354,365,370,383,394],{"__ignoreMap":287},[291,292,294,298,302,305],"span",{"class":145,"line":293},1,[291,295,297],{"class":296},"snl16","from",[291,299,301],{"class":300},"s95oV"," qgis.core ",[291,303,304],{"class":296},"import",[291,306,307],{"class":300}," QgsProject, QgsWkbTypes\n",[291,309,311],{"class":145,"line":310},2,[291,312,314],{"emptyLinePlaceholder":313},true,"\n",[291,316,318,321,324,327,331,334,337],{"class":145,"line":317},3,[291,319,320],{"class":300},"layer ",[291,322,323],{"class":296},"=",[291,325,326],{"class":300}," QgsProject.instance().mapLayersByName(",[291,328,330],{"class":329},"sU2Wk","\"parcels\"",[291,332,333],{"class":300},")[",[291,335,60],{"class":336},"sDLfK",[291,338,339],{"class":300},"]\n",[291,341,343,346,348,351],{"class":145,"line":342},4,[291,344,345],{"class":300},"feature ",[291,347,323],{"class":296},[291,349,350],{"class":336}," next",[291,352,353],{"class":300},"(layer.getFeatures())\n",[291,355,357,360,362],{"class":145,"line":356},5,[291,358,359],{"class":300},"geometry ",[291,361,323],{"class":296},[291,363,364],{"class":300}," feature.geometry()\n",[291,366,368],{"class":145,"line":367},6,[291,369,314],{"emptyLinePlaceholder":313},[291,371,373,376,379],{"class":145,"line":372},7,[291,374,375],{"class":336},"print",[291,377,378],{"class":300},"(QgsWkbTypes.displayString(geometry.wkbType()))  ",[291,380,382],{"class":381},"sjoCn","# e.g. \"Polygon\"\n",[291,384,386,388,391],{"class":145,"line":385},8,[291,387,375],{"class":336},[291,389,390],{"class":300},"(geometry.isMultipart())                          ",[291,392,393],{"class":381},"# False for a single polygon\n",[291,395,397,399,402],{"class":145,"line":396},9,[291,398,375],{"class":336},[291,400,401],{"class":300},"(geometry.constGet().vertexCount())               ",[291,403,404],{"class":381},"# coordinate count\n",[14,406,407,410,411,414,415,418,419,422,423,425],{},[220,408,409],{},"Breakdown:"," ",[18,412,413],{},"wkbType()"," returns a numeric enum; ",[18,416,417],{},"QgsWkbTypes.displayString()"," turns it into readable text, which is far more useful in a log line. ",[18,420,421],{},"isMultipart()"," is the check that prevents the single most common geometry bug — a script that assumes one part and silently processes only the first. ",[18,424,143],{}," gives read-only access to the underlying geometry when you need the vertex count or an individual coordinate.",[14,427,428,429,431,432,435],{},"The single-versus-multi distinction deserves respect. A shapefile of \"buildings\" may contain both ",[18,430,199],{}," and ",[18,433,434],{},"MultiPolygon"," features in the same layer, because the format allows it. Rather than branching everywhere, normalise early:",[282,437,439],{"className":284,"code":438,"language":286,"meta":287,"style":287},"parts = list(geometry.parts()) if geometry.isMultipart() else [geometry.constGet()]\nfor part in parts:\n    print(part.area())\n",[18,440,441,466,480],{"__ignoreMap":287},[291,442,443,446,448,451,454,457,460,463],{"class":145,"line":293},[291,444,445],{"class":300},"parts ",[291,447,323],{"class":296},[291,449,450],{"class":336}," list",[291,452,453],{"class":300},"(geometry.parts()) ",[291,455,456],{"class":296},"if",[291,458,459],{"class":300}," geometry.isMultipart() ",[291,461,462],{"class":296},"else",[291,464,465],{"class":300}," [geometry.constGet()]\n",[291,467,468,471,474,477],{"class":145,"line":310},[291,469,470],{"class":296},"for",[291,472,473],{"class":300}," part ",[291,475,476],{"class":296},"in",[291,478,479],{"class":300}," parts:\n",[291,481,482,485],{"class":145,"line":317},[291,483,484],{"class":336},"    print",[291,486,487],{"class":300},"(part.area())\n",[14,489,490,410,492,495],{},[220,491,409],{},[18,493,494],{},"parts()"," yields each component of a multi-part geometry. Wrapping the single-part case in a one-element list means the loop body is written once. This is the pattern behind nearly every robust geometry utility in the QGIS codebase.",[209,497,499],{"id":498},"constructive-operations","Constructive operations",[14,501,502,503,505],{},"Constructive operations take one or two geometries and return a new one. They are the building blocks that layer-level algorithms compose. All of them return a fresh ",[18,504,20],{}," and leave the input untouched.",[14,507,508],{},[43,509,512,515,518,521,524,547,567,586],{"viewBox":510,"role":46,"ariaLabel":511,"xmlns":48},"0 0 760 260","Four constructive geometry operations applied to the same L-shaped polygon: buffer expands it outward, centroid reduces it to a point, convex hull wraps it in the smallest enclosing convex shape, and simplify removes vertices",[50,513,514],{},"Constructive operations on one polygon",[54,516,517],{},"The same L-shaped source polygon shown four times in outline. Buffer adds a rounded expansion around it, centroid marks a single interior point, convex hull draws the smallest convex shape enclosing it, and simplify draws a version with fewer vertices.",[58,519],{"x":60,"y":60,"width":61,"height":520,"fill":63},"260",[82,522,523],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"One source polygon, four results — the input is never modified",[525,526,527,533,537,542,544],"g",{},[58,528],{"x":529,"y":530,"width":531,"height":532,"rx":96,"fill":97,"stroke":116,"style":164},"16","44","176","196",[82,534,536],{"x":535,"y":95,"style":104,"fill":116,"textAnchor":88},"104","buffer(12)",[77,538],{"d":539,"fill":116,"fillOpacity":540,"stroke":116,"style":541},"M48 186 L48 108 L92 108 L92 138 L152 138 L152 186 Z",0.18,"stroke-width:14;stroke-linejoin:round",[77,543],{"d":539,"fill":97,"stroke":98,"style":204},[82,545,546],{"x":535,"y":113,"style":109,"fill":80,"textAnchor":88},"grows by a distance",[525,548,549,552,556,560,564],{},[58,550],{"x":551,"y":530,"width":531,"height":532,"rx":96,"fill":97,"stroke":133,"style":164},"208",[82,553,555],{"x":554,"y":95,"style":104,"fill":133,"textAnchor":88},"296","centroid()",[77,557],{"d":558,"fill":98,"fillOpacity":559,"stroke":98,"style":204},"M240 186 L240 108 L284 108 L284 138 L344 138 L344 186 Z",0.08,[172,561],{"cx":562,"cy":563,"r":72,"fill":133},"288","160",[82,565,566],{"x":554,"y":113,"style":109,"fill":80,"textAnchor":88},"may fall outside the shape",[525,568,569,572,576,580,583],{},[58,570],{"x":571,"y":530,"width":531,"height":532,"rx":96,"fill":97,"stroke":163,"style":164},"400",[82,573,575],{"x":574,"y":95,"style":104,"fill":163,"textAnchor":88},"488","convexHull()",[77,577],{"d":578,"fill":163,"fillOpacity":579,"stroke":163,"style":204},"M432 186 L432 108 L476 108 L536 138 L536 186 Z",0.2,[77,581],{"d":582,"fill":188,"stroke":98,"style":204},"M432 186 L432 108 L476 108 L476 138 L536 138 L536 186 Z",[82,584,585],{"x":574,"y":113,"style":109,"fill":80,"textAnchor":88},"smallest convex wrapper",[525,587,588,591,595,600,604],{},[58,589],{"x":590,"y":530,"width":158,"height":532,"rx":96,"fill":97,"stroke":98,"style":164},"592",[82,592,594],{"x":593,"y":95,"style":104,"fill":98,"textAnchor":88},"668","simplify(8)",[77,596],{"d":597,"fill":188,"stroke":598,"style":599},"M616 186 L616 108 L652 112 L648 140 L712 136 L716 186 Z","#59645f","stroke-width:1.5;stroke-dasharray:4 3;stroke-linejoin:round",[77,601],{"d":602,"fill":98,"fillOpacity":603,"stroke":98,"style":204},"M616 186 L616 108 L712 136 L716 186 Z",0.16,[82,605,606],{"x":593,"y":113,"style":109,"fill":80,"textAnchor":88},"drops vertices, keeps shape",[282,608,610],{"className":284,"code":609,"language":286,"meta":287,"style":287},"from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\ngeometry = next(layer.getFeatures()).geometry()\n\nbuffered = geometry.buffer(25.0, 8)      # 25 map units, 8 segments per quarter circle\ncentroid = geometry.centroid()\nhull = geometry.convexHull()\nsimplified = geometry.simplify(5.0)      # Douglas-Peucker tolerance in map units\n\nprint(buffered.area(), geometry.area())\n",[18,611,612,623,627,643,654,658,682,692,702,720,725],{"__ignoreMap":287},[291,613,614,616,618,620],{"class":145,"line":293},[291,615,297],{"class":296},[291,617,301],{"class":300},[291,619,304],{"class":296},[291,621,622],{"class":300}," QgsProject\n",[291,624,625],{"class":145,"line":310},[291,626,314],{"emptyLinePlaceholder":313},[291,628,629,631,633,635,637,639,641],{"class":145,"line":317},[291,630,320],{"class":300},[291,632,323],{"class":296},[291,634,326],{"class":300},[291,636,330],{"class":329},[291,638,333],{"class":300},[291,640,60],{"class":336},[291,642,339],{"class":300},[291,644,645,647,649,651],{"class":145,"line":342},[291,646,359],{"class":300},[291,648,323],{"class":296},[291,650,350],{"class":336},[291,652,653],{"class":300},"(layer.getFeatures()).geometry()\n",[291,655,656],{"class":145,"line":356},[291,657,314],{"emptyLinePlaceholder":313},[291,659,660,663,665,668,671,674,676,679],{"class":145,"line":367},[291,661,662],{"class":300},"buffered ",[291,664,323],{"class":296},[291,666,667],{"class":300}," geometry.buffer(",[291,669,670],{"class":336},"25.0",[291,672,673],{"class":300},", ",[291,675,72],{"class":336},[291,677,678],{"class":300},")      ",[291,680,681],{"class":381},"# 25 map units, 8 segments per quarter circle\n",[291,683,684,687,689],{"class":145,"line":372},[291,685,686],{"class":300},"centroid ",[291,688,323],{"class":296},[291,690,691],{"class":300}," geometry.centroid()\n",[291,693,694,697,699],{"class":145,"line":385},[291,695,696],{"class":300},"hull ",[291,698,323],{"class":296},[291,700,701],{"class":300}," geometry.convexHull()\n",[291,703,704,707,709,712,715,717],{"class":145,"line":396},[291,705,706],{"class":300},"simplified ",[291,708,323],{"class":296},[291,710,711],{"class":300}," geometry.simplify(",[291,713,714],{"class":336},"5.0",[291,716,678],{"class":300},[291,718,719],{"class":381},"# Douglas-Peucker tolerance in map units\n",[291,721,723],{"class":145,"line":722},10,[291,724,314],{"emptyLinePlaceholder":313},[291,726,728,730],{"class":145,"line":727},11,[291,729,375],{"class":336},[291,731,732],{"class":300},"(buffered.area(), geometry.area())\n",[14,734,735,410,737,740,741,745,746,749],{},[220,736,409],{},[18,738,739],{},"buffer()"," takes a distance in ",[742,743,744],"em",{},"map units"," — metres in a projected CRS, degrees in a geographic one, which is why the CRS warning above matters. The second argument controls how many straight segments approximate each quarter of a rounded corner; 8 is a reasonable default, and lowering it produces visibly faceted output. ",[18,747,748],{},"simplify()"," uses the Douglas-Peucker algorithm and its tolerance is also in map units.",[14,751,752,753,755,756,759,760,431,764,768],{},"A subtlety worth flagging: ",[18,754,555],{}," can land outside a concave polygon, which makes it a poor choice for label placement. ",[18,757,758],{},"pointOnSurface()"," guarantees a point inside the geometry and is what labelling engines actually use. The focused recipes ",[26,761,763],{"href":762},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuffer-geometry-pyqgis\u002F","Buffer a Geometry in PyQGIS",[26,765,767],{"href":766},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fsimplify-geometry-pyqgis\u002F","Simplify Geometry in PyQGIS"," go into the parameter trade-offs.",[209,770,772],{"id":771},"spatial-predicates","Spatial predicates",[14,774,775,776,277,779,673,782,673,785,673,788,673,791,673,794,673,797,800,801,804],{},"Predicates answer yes\u002Fno questions about how two geometries relate. PyQGIS exposes them as methods that return a plain ",[18,777,778],{},"bool",[18,780,781],{},"intersects()",[18,783,784],{},"contains()",[18,786,787],{},"within()",[18,789,790],{},"touches()",[18,792,793],{},"crosses()",[18,795,796],{},"overlaps()",[18,798,799],{},"disjoint()",", and ",[18,802,803],{},"equals()",". They are cheap, they are the basis of every spatial join, and their edge-case behaviour is where the bugs live.",[14,806,807],{},[43,808,811,814,817,820,823,843,855,869,888,904,919,969],{"viewBox":809,"role":46,"ariaLabel":810,"xmlns":48},"0 0 760 300","A matrix of spatial predicates showing which relationships hold between two shapes in five configurations: separate, touching at a boundary, partially overlapping, one inside the other, and identical",[50,812,813],{},"Which predicate is true in each configuration",[54,815,816],{},"Five configurations of a blue shape and a teal shape are shown across the top: disjoint, touching at an edge, partially overlapping, one fully contained in the other, and identical. Beneath each, a column of ticks and crosses records whether intersects, touches, within and overlaps return true.",[58,818],{"x":60,"y":60,"width":61,"height":819,"fill":63},"300",[82,821,822],{"x":84,"y":92,"style":86,"fill":87,"textAnchor":88},"Touching counts as intersecting — but not as overlapping",[525,824,825,831,837,841],{},[58,826],{"x":529,"y":827,"width":828,"height":829,"rx":72,"fill":97,"stroke":598,"style":830},"40","136","96","stroke-width:1.5",[82,832,836],{"x":833,"y":834,"style":835,"fill":87,"textAnchor":88},"84","60","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","disjoint",[58,838],{"x":839,"y":840,"width":530,"height":530,"fill":98,"fillOpacity":579,"stroke":98,"style":164},"32","76",[58,842],{"x":829,"y":840,"width":530,"height":530,"fill":116,"fillOpacity":579,"stroke":116,"style":164},[525,844,845,847,851,853],{},[58,846],{"x":161,"y":827,"width":828,"height":829,"rx":72,"fill":97,"stroke":598,"style":830},[82,848,850],{"x":849,"y":834,"style":835,"fill":87,"textAnchor":88},"232","touches",[58,852],{"x":168,"y":840,"width":530,"height":530,"fill":98,"fillOpacity":579,"stroke":98,"style":164},[58,854],{"x":849,"y":840,"width":530,"height":530,"fill":116,"fillOpacity":579,"stroke":116,"style":164},[525,856,857,860,863,866],{},[58,858],{"x":859,"y":827,"width":828,"height":829,"rx":72,"fill":97,"stroke":598,"style":830},"312",[82,861,862],{"x":84,"y":834,"style":835,"fill":87,"textAnchor":88},"overlaps",[58,864],{"x":865,"y":840,"width":93,"height":530,"fill":98,"fillOpacity":579,"stroke":98,"style":164},"332",[58,867],{"x":868,"y":840,"width":93,"height":530,"fill":116,"fillOpacity":579,"stroke":116,"style":164},"366",[525,870,871,874,877,882],{},[58,872],{"x":873,"y":827,"width":828,"height":829,"rx":72,"fill":97,"stroke":598,"style":830},"460",[82,875,876],{"x":136,"y":834,"style":835,"fill":87,"textAnchor":88},"within",[58,878],{"x":879,"y":880,"width":881,"height":93,"fill":116,"fillOpacity":579,"stroke":116,"style":164},"482","72","92",[58,883],{"x":884,"y":885,"width":886,"height":92,"fill":98,"fillOpacity":887,"stroke":98,"style":164},"506","86","42",0.35,[525,889,890,893,897,901],{},[58,891],{"x":892,"y":827,"width":828,"height":829,"rx":72,"fill":97,"stroke":598,"style":830},"608",[82,894,896],{"x":895,"y":834,"style":835,"fill":87,"textAnchor":88},"676","equals",[58,898],{"x":899,"y":840,"width":93,"height":530,"fill":116,"fillOpacity":579,"stroke":116,"style":900},"650","stroke-width:4",[58,902],{"x":899,"y":840,"width":93,"height":530,"fill":98,"fillOpacity":579,"stroke":98,"style":903},"stroke-width:2;stroke-dasharray:5 4",[525,905,906,910,913,916],{},[82,907,781],{"x":840,"y":531,"style":908,"fill":80,"textAnchor":909},"text-anchor:end;font-size:11.5px;font-family:sans-serif","end",[82,911,790],{"x":840,"y":912,"style":908,"fill":80,"textAnchor":909},"206",[82,914,787],{"x":840,"y":915,"style":908,"fill":80,"textAnchor":909},"236",[82,917,796],{"x":840,"y":918,"style":908,"fill":80,"textAnchor":909},"266",[525,920,922,927,929,931,933,937,939,941,943,946,948,950,952,954,956,958,960,963,965,967],{"style":921},"font-size:14px;font-weight:bold;font-family:sans-serif;text-anchor:middle",[82,923,926],{"x":924,"y":531,"fill":925},"140","#b91c1c","✗",[82,928,926],{"x":924,"y":912,"fill":925},[82,930,926],{"x":924,"y":915,"fill":925},[82,932,926],{"x":924,"y":918,"fill":925},[82,934,936],{"x":935,"y":531,"fill":163},"264","✓",[82,938,936],{"x":935,"y":912,"fill":163},[82,940,926],{"x":935,"y":915,"fill":925},[82,942,926],{"x":935,"y":918,"fill":925},[82,944,936],{"x":945,"y":531,"fill":163},"392",[82,947,926],{"x":945,"y":912,"fill":925},[82,949,926],{"x":945,"y":915,"fill":925},[82,951,936],{"x":945,"y":918,"fill":163},[82,953,936],{"x":195,"y":531,"fill":163},[82,955,926],{"x":195,"y":912,"fill":925},[82,957,936],{"x":195,"y":915,"fill":163},[82,959,926],{"x":195,"y":918,"fill":925},[82,961,936],{"x":962,"y":531,"fill":163},"648",[82,964,926],{"x":962,"y":912,"fill":925},[82,966,936],{"x":962,"y":915,"fill":163},[82,968,926],{"x":962,"y":918,"fill":925},[82,970,973],{"x":84,"y":971,"style":972,"fill":598,"textAnchor":88},"290","text-anchor:middle;font-size:11px;font-family:sans-serif","within() is true for identical geometries — a shape contains itself",[14,975,976,977,980,981,983,984,987,988,277,991,993,994,997,998,1003,1004,1006],{},"Three results in that matrix routinely surprise people. ",[220,978,979],{},"Touching counts as intersecting",": two parcels sharing a boundary satisfy ",[18,982,781],{}," even though their interiors never meet, which is why a spatial join on ",[18,985,986],{},"intersects"," double-counts features along shared edges. ",[220,989,990],{},"Overlapping is stricter than intersecting",[18,992,796],{}," requires the interiors to share area ",[742,995,996],{},"and"," neither shape to contain the other. And ",[220,999,1000,1002],{},[18,1001,787],{}," is reflexive",": a geometry is within itself, so a self-join on ",[18,1005,876],{}," matches every feature to itself unless you filter on feature ID.",[282,1008,1010],{"className":284,"code":1009,"language":286,"meta":287,"style":287},"from qgis.core import QgsProject\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nzones = QgsProject.instance().mapLayersByName(\"flood_zones\")[0]\n\nzone_geometry = next(zones.getFeatures()).geometry()\n\nfor parcel in parcels.getFeatures():\n    geometry = parcel.geometry()\n    if geometry.within(zone_geometry):\n        status = \"entirely inside\"\n    elif geometry.intersects(zone_geometry):\n        status = \"partially affected\"\n    else:\n        continue\n    print(parcel[\"parcel_id\"], status)\n",[18,1011,1012,1022,1026,1043,1061,1065,1077,1081,1093,1103,1111,1121,1130,1140,1149,1155],{"__ignoreMap":287},[291,1013,1014,1016,1018,1020],{"class":145,"line":293},[291,1015,297],{"class":296},[291,1017,301],{"class":300},[291,1019,304],{"class":296},[291,1021,622],{"class":300},[291,1023,1024],{"class":145,"line":310},[291,1025,314],{"emptyLinePlaceholder":313},[291,1027,1028,1031,1033,1035,1037,1039,1041],{"class":145,"line":317},[291,1029,1030],{"class":300},"parcels ",[291,1032,323],{"class":296},[291,1034,326],{"class":300},[291,1036,330],{"class":329},[291,1038,333],{"class":300},[291,1040,60],{"class":336},[291,1042,339],{"class":300},[291,1044,1045,1048,1050,1052,1055,1057,1059],{"class":145,"line":342},[291,1046,1047],{"class":300},"zones ",[291,1049,323],{"class":296},[291,1051,326],{"class":300},[291,1053,1054],{"class":329},"\"flood_zones\"",[291,1056,333],{"class":300},[291,1058,60],{"class":336},[291,1060,339],{"class":300},[291,1062,1063],{"class":145,"line":356},[291,1064,314],{"emptyLinePlaceholder":313},[291,1066,1067,1070,1072,1074],{"class":145,"line":367},[291,1068,1069],{"class":300},"zone_geometry ",[291,1071,323],{"class":296},[291,1073,350],{"class":336},[291,1075,1076],{"class":300},"(zones.getFeatures()).geometry()\n",[291,1078,1079],{"class":145,"line":372},[291,1080,314],{"emptyLinePlaceholder":313},[291,1082,1083,1085,1088,1090],{"class":145,"line":385},[291,1084,470],{"class":296},[291,1086,1087],{"class":300}," parcel ",[291,1089,476],{"class":296},[291,1091,1092],{"class":300}," parcels.getFeatures():\n",[291,1094,1095,1098,1100],{"class":145,"line":396},[291,1096,1097],{"class":300},"    geometry ",[291,1099,323],{"class":296},[291,1101,1102],{"class":300}," parcel.geometry()\n",[291,1104,1105,1108],{"class":145,"line":722},[291,1106,1107],{"class":296},"    if",[291,1109,1110],{"class":300}," geometry.within(zone_geometry):\n",[291,1112,1113,1116,1118],{"class":145,"line":727},[291,1114,1115],{"class":300},"        status ",[291,1117,323],{"class":296},[291,1119,1120],{"class":329}," \"entirely inside\"\n",[291,1122,1124,1127],{"class":145,"line":1123},12,[291,1125,1126],{"class":296},"    elif",[291,1128,1129],{"class":300}," geometry.intersects(zone_geometry):\n",[291,1131,1133,1135,1137],{"class":145,"line":1132},13,[291,1134,1115],{"class":300},[291,1136,323],{"class":296},[291,1138,1139],{"class":329}," \"partially affected\"\n",[291,1141,1143,1146],{"class":145,"line":1142},14,[291,1144,1145],{"class":296},"    else",[291,1147,1148],{"class":300},":\n",[291,1150,1152],{"class":145,"line":1151},15,[291,1153,1154],{"class":296},"        continue\n",[291,1156,1158,1160,1163,1166],{"class":145,"line":1157},16,[291,1159,484],{"class":336},[291,1161,1162],{"class":300},"(parcel[",[291,1164,1165],{"class":329},"\"parcel_id\"",[291,1167,1168],{"class":300},"], status)\n",[14,1170,1171,1173,1174,1176,1177,1179,1180,1182,1183,1187],{},[220,1172,409],{}," Testing ",[18,1175,787],{}," before ",[18,1178,781],{}," matters — the order encodes the specificity, because every geometry that is within another also intersects it. Skipping the ",[18,1181,462],{}," branch entirely avoids materialising results for the majority of features that are irrelevant. ",[26,1184,1186],{"href":1185},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis\u002F","Test Whether Two Geometries Intersect in PyQGIS"," unpacks the boundary cases with runnable examples.",[209,1189,1191],{"id":1190},"overlay-operations-between-two-geometries","Overlay operations between two geometries",[14,1193,1194,1195,1198,1199,1202,1203,673,1206,1209,1210,1213,1214,1216],{},"Predicates answer ",[742,1196,1197],{},"whether"," two geometries relate; overlay operations produce the geometry that expresses ",[742,1200,1201],{},"how",". The three that matter are ",[18,1204,1205],{},"intersection()",[18,1207,1208],{},"combine()"," (union), and ",[18,1211,1212],{},"difference()",", and at geometry level they are the exact operations the layer-level algorithms in ",[26,1215,34],{"href":33}," apply feature by feature.",[14,1218,1219],{},[43,1220,1223,1226,1229,1232,1256,1259,1286,1309],{"viewBox":1221,"role":46,"ariaLabel":1222,"xmlns":48},"0 0 760 226","Three overlay operations on the same pair of overlapping circles: intersection keeps only the shared region, union merges both into one shape, and difference keeps the first circle minus the second",[50,1224,1225],{},"Intersection, union and difference on one pair of shapes",[54,1227,1228],{},"The same two overlapping circles, labelled A on the left and B on the right, are shown three times. Intersection fills only the lens-shaped region where they overlap. Union fills the whole combined outline. Difference fills the part of A that lies outside B.",[58,1230],{"x":60,"y":60,"width":61,"height":1231,"fill":63},"226",[65,1233,1234,1246],{},[1235,1236,1238,1241],"mask",{"id":1237},"geoOvA",[58,1239],{"x":60,"y":60,"width":61,"height":1231,"fill":1240},"#000000",[172,1242],{"cx":1243,"cy":167,"r":1244,"fill":1245},"120","48","#ffffff",[1235,1247,1249,1253],{"id":1248},"geoOvMask",[58,1250],{"x":1251,"y":60,"width":1252,"height":1231,"fill":1245},"512","240",[172,1254],{"cx":1255,"cy":167,"r":1244,"fill":1240},"662",[82,1257,1258],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Same inputs, three different results — order matters only for difference",[525,1260,1261,1263,1267,1272,1274,1276,1279,1282],{},[58,1262],{"x":529,"y":530,"width":849,"height":161,"rx":96,"fill":97,"stroke":116,"style":164},[82,1264,1266],{"x":167,"y":1265,"style":104,"fill":116,"textAnchor":88},"68","a.intersection(b)",[172,1268],{"cx":1269,"cy":167,"r":1244,"fill":116,"fillOpacity":1270,"style":1271},"158",0.5,"mask:url(#geoOvA)",[172,1273],{"cx":1243,"cy":167,"r":1244,"fill":188,"stroke":98,"style":99},[172,1275],{"cx":1269,"cy":167,"r":1244,"fill":188,"stroke":133,"style":99},[82,1277,1278],{"x":833,"y":828,"style":169,"fill":98,"textAnchor":88},"A",[82,1280,1281],{"x":532,"y":828,"style":169,"fill":133,"textAnchor":88},"B",[82,1283,1285],{"x":167,"y":1284,"style":109,"fill":80,"textAnchor":88},"198","the shared region only",[525,1287,1288,1291,1295,1299,1302,1304,1306],{},[58,1289],{"x":1290,"y":530,"width":849,"height":161,"rx":96,"fill":97,"stroke":163,"style":164},"268",[82,1292,1294],{"x":1293,"y":1265,"style":104,"fill":163,"textAnchor":88},"384","a.combine(b)",[172,1296],{"cx":1297,"cy":167,"r":1244,"fill":163,"fillOpacity":1298},"372",0.4,[172,1300],{"cx":1301,"cy":167,"r":1244,"fill":163,"fillOpacity":1298},"410",[172,1303],{"cx":1297,"cy":167,"r":1244,"fill":188,"stroke":98,"style":99},[172,1305],{"cx":1301,"cy":167,"r":1244,"fill":188,"stroke":133,"style":99},[82,1307,1308],{"x":1293,"y":1284,"style":109,"fill":80,"textAnchor":88},"one merged geometry",[525,1310,1311,1313,1317,1322,1324,1327],{},[58,1312],{"x":195,"y":530,"width":849,"height":161,"rx":96,"fill":97,"stroke":133,"style":164},[82,1314,1316],{"x":1315,"y":1265,"style":104,"fill":133,"textAnchor":88},"636","a.difference(b)",[172,1318],{"cx":1319,"cy":167,"r":1244,"fill":133,"fillOpacity":1320,"style":1321},"624",0.45,"mask:url(#geoOvMask)",[172,1323],{"cx":1319,"cy":167,"r":1244,"fill":188,"stroke":98,"style":99},[172,1325],{"cx":1255,"cy":167,"r":1244,"fill":188,"stroke":133,"style":1326},"stroke-width:2.5;stroke-dasharray:5 4",[82,1328,1329],{"x":1315,"y":1284,"style":109,"fill":80,"textAnchor":88},"A minus B — not commutative",[282,1331,1333],{"className":284,"code":1332,"language":286,"meta":287,"style":287},"from qgis.core import QgsGeometry, QgsProject\n\nzones = QgsProject.instance().mapLayersByName(\"flood_zones\")[0]\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nflood = next(zones.getFeatures()).geometry()\n\nfor parcel in parcels.getFeatures():\n    geometry = parcel.geometry()\n    if not geometry.intersects(flood):\n        continue\n    inside = geometry.intersection(flood)\n    outside = geometry.difference(flood)\n    share = inside.area() \u002F geometry.area() if geometry.area() else 0\n    print(f\"{parcel['parcel_id']}: {share:.1%} in the flood zone, \"\n          f\"{outside.area():.0f} m² outside\")\n",[18,1334,1335,1346,1350,1366,1382,1386,1397,1401,1411,1419,1429,1433,1443,1453,1478,1521],{"__ignoreMap":287},[291,1336,1337,1339,1341,1343],{"class":145,"line":293},[291,1338,297],{"class":296},[291,1340,301],{"class":300},[291,1342,304],{"class":296},[291,1344,1345],{"class":300}," QgsGeometry, QgsProject\n",[291,1347,1348],{"class":145,"line":310},[291,1349,314],{"emptyLinePlaceholder":313},[291,1351,1352,1354,1356,1358,1360,1362,1364],{"class":145,"line":317},[291,1353,1047],{"class":300},[291,1355,323],{"class":296},[291,1357,326],{"class":300},[291,1359,1054],{"class":329},[291,1361,333],{"class":300},[291,1363,60],{"class":336},[291,1365,339],{"class":300},[291,1367,1368,1370,1372,1374,1376,1378,1380],{"class":145,"line":342},[291,1369,1030],{"class":300},[291,1371,323],{"class":296},[291,1373,326],{"class":300},[291,1375,330],{"class":329},[291,1377,333],{"class":300},[291,1379,60],{"class":336},[291,1381,339],{"class":300},[291,1383,1384],{"class":145,"line":356},[291,1385,314],{"emptyLinePlaceholder":313},[291,1387,1388,1391,1393,1395],{"class":145,"line":367},[291,1389,1390],{"class":300},"flood ",[291,1392,323],{"class":296},[291,1394,350],{"class":336},[291,1396,1076],{"class":300},[291,1398,1399],{"class":145,"line":372},[291,1400,314],{"emptyLinePlaceholder":313},[291,1402,1403,1405,1407,1409],{"class":145,"line":385},[291,1404,470],{"class":296},[291,1406,1087],{"class":300},[291,1408,476],{"class":296},[291,1410,1092],{"class":300},[291,1412,1413,1415,1417],{"class":145,"line":396},[291,1414,1097],{"class":300},[291,1416,323],{"class":296},[291,1418,1102],{"class":300},[291,1420,1421,1423,1426],{"class":145,"line":722},[291,1422,1107],{"class":296},[291,1424,1425],{"class":296}," not",[291,1427,1428],{"class":300}," geometry.intersects(flood):\n",[291,1430,1431],{"class":145,"line":727},[291,1432,1154],{"class":296},[291,1434,1435,1438,1440],{"class":145,"line":1123},[291,1436,1437],{"class":300},"    inside ",[291,1439,323],{"class":296},[291,1441,1442],{"class":300}," geometry.intersection(flood)\n",[291,1444,1445,1448,1450],{"class":145,"line":1132},[291,1446,1447],{"class":300},"    outside ",[291,1449,323],{"class":296},[291,1451,1452],{"class":300}," geometry.difference(flood)\n",[291,1454,1455,1458,1460,1463,1466,1469,1471,1473,1475],{"class":145,"line":1142},[291,1456,1457],{"class":300},"    share ",[291,1459,323],{"class":296},[291,1461,1462],{"class":300}," inside.area() ",[291,1464,1465],{"class":296},"\u002F",[291,1467,1468],{"class":300}," geometry.area() ",[291,1470,456],{"class":296},[291,1472,1468],{"class":300},[291,1474,462],{"class":296},[291,1476,1477],{"class":336}," 0\n",[291,1479,1480,1482,1485,1488,1491,1494,1497,1500,1503,1506,1508,1510,1513,1516,1518],{"class":145,"line":1151},[291,1481,484],{"class":336},[291,1483,1484],{"class":300},"(",[291,1486,1487],{"class":296},"f",[291,1489,1490],{"class":329},"\"",[291,1492,1493],{"class":336},"{",[291,1495,1496],{"class":300},"parcel[",[291,1498,1499],{"class":329},"'parcel_id'",[291,1501,1502],{"class":300},"]",[291,1504,1505],{"class":336},"}",[291,1507,277],{"class":329},[291,1509,1493],{"class":336},[291,1511,1512],{"class":300},"share",[291,1514,1515],{"class":296},":.1%",[291,1517,1505],{"class":336},[291,1519,1520],{"class":329}," in the flood zone, \"\n",[291,1522,1523,1526,1528,1530,1533,1536,1538,1541],{"class":145,"line":1157},[291,1524,1525],{"class":296},"          f",[291,1527,1490],{"class":329},[291,1529,1493],{"class":336},[291,1531,1532],{"class":300},"outside.area()",[291,1534,1535],{"class":296},":.0f",[291,1537,1505],{"class":336},[291,1539,1540],{"class":329}," m² outside\"",[291,1542,1543],{"class":300},")\n",[14,1545,1546,1548,1549,1551,1552,1554,1555,431,1557,1559,1560,1563],{},[220,1547,409],{}," Guarding with ",[18,1550,781],{}," before calling ",[18,1553,1205],{}," is not just tidiness — for disjoint inputs the overlay still runs the full GEOS computation before returning an empty geometry, so the cheap predicate is a real saving on large layers. ",[18,1556,1205],{},[18,1558,1212],{}," both return new geometries; neither modifies the inputs. The ",[18,1561,1562],{},"if geometry.area() else 0"," guard prevents a division error on a degenerate zero-area polygon, which turns up more often than you would expect in digitised data.",[14,1565,1566,1568],{},[18,1567,1208],{}," is the union, and its most common use is dissolving many geometries into one. Doing that in a loop is quadratic — each step rebuilds a progressively larger polygon — so for more than a handful of parts, collect them and use the static form:",[282,1570,1572],{"className":284,"code":1571,"language":286,"meta":287,"style":287},"geometries = [f.geometry() for f in parcels.getFeatures()]\ndissolved = QgsGeometry.unaryUnion(geometries)\nprint(dissolved.area(), \"m² total, \", len(list(dissolved.parts())), \"part(s)\")\n",[18,1573,1574,1594,1604],{"__ignoreMap":287},[291,1575,1576,1579,1581,1584,1586,1589,1591],{"class":145,"line":293},[291,1577,1578],{"class":300},"geometries ",[291,1580,323],{"class":296},[291,1582,1583],{"class":300}," [f.geometry() ",[291,1585,470],{"class":296},[291,1587,1588],{"class":300}," f ",[291,1590,476],{"class":296},[291,1592,1593],{"class":300}," parcels.getFeatures()]\n",[291,1595,1596,1599,1601],{"class":145,"line":310},[291,1597,1598],{"class":300},"dissolved ",[291,1600,323],{"class":296},[291,1602,1603],{"class":300}," QgsGeometry.unaryUnion(geometries)\n",[291,1605,1606,1608,1611,1614,1616,1619,1621,1624,1627,1630],{"class":145,"line":317},[291,1607,375],{"class":336},[291,1609,1610],{"class":300},"(dissolved.area(), ",[291,1612,1613],{"class":329},"\"m² total, \"",[291,1615,673],{"class":300},[291,1617,1618],{"class":336},"len",[291,1620,1484],{"class":300},[291,1622,1623],{"class":336},"list",[291,1625,1626],{"class":300},"(dissolved.parts())), ",[291,1628,1629],{"class":329},"\"part(s)\"",[291,1631,1543],{"class":300},[14,1633,1634,410,1636,1639,1640,1643,1644,1646],{},[220,1635,409],{},[18,1637,1638],{},"unaryUnion()"," unions the whole collection in one optimised GEOS pass instead of ",[742,1641,1642],{},"n"," pairwise combines. The result is a multi-polygon whenever the inputs are not all contiguous, which is why counting ",[18,1645,494],{}," afterwards is a useful sanity check — a \"dissolve\" that returns forty parts usually means the inputs had gaps you did not know about.",[209,1648,1650],{"id":1649},"working-at-the-vertex-level","Working at the vertex level",[14,1652,1653,1654,1656],{},"Sometimes the operation you want has no name in GEOS: snapping a stray node, dropping every third vertex, or rewriting Z values. ",[18,1655,20],{}," exposes vertex-level access for those cases, with an index scheme that runs continuously across all parts and rings.",[282,1658,1660],{"className":284,"code":1659,"language":286,"meta":287,"style":287},"from qgis.core import QgsGeometry, QgsPointXY, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"survey_lines\")[0]\nfeature = next(layer.getFeatures())\ngeometry = QgsGeometry(feature.geometry())   # explicit copy — do not edit in place\n\nfor index, vertex in enumerate(geometry.vertices()):\n    print(index, round(vertex.x(), 2), round(vertex.y(), 2))\n\ngeometry.moveVertex(QgsPointXY(0, 0).x(), QgsPointXY(0, 0).y(), 0)\ngeometry.deleteVertex(1)\nprint(geometry.asWkt(precision=2))\n",[18,1661,1662,1673,1677,1694,1704,1716,1720,1735,1764,1768,1795,1805],{"__ignoreMap":287},[291,1663,1664,1666,1668,1670],{"class":145,"line":293},[291,1665,297],{"class":296},[291,1667,301],{"class":300},[291,1669,304],{"class":296},[291,1671,1672],{"class":300}," QgsGeometry, QgsPointXY, QgsProject\n",[291,1674,1675],{"class":145,"line":310},[291,1676,314],{"emptyLinePlaceholder":313},[291,1678,1679,1681,1683,1685,1688,1690,1692],{"class":145,"line":317},[291,1680,320],{"class":300},[291,1682,323],{"class":296},[291,1684,326],{"class":300},[291,1686,1687],{"class":329},"\"survey_lines\"",[291,1689,333],{"class":300},[291,1691,60],{"class":336},[291,1693,339],{"class":300},[291,1695,1696,1698,1700,1702],{"class":145,"line":342},[291,1697,345],{"class":300},[291,1699,323],{"class":296},[291,1701,350],{"class":336},[291,1703,353],{"class":300},[291,1705,1706,1708,1710,1713],{"class":145,"line":356},[291,1707,359],{"class":300},[291,1709,323],{"class":296},[291,1711,1712],{"class":300}," QgsGeometry(feature.geometry())   ",[291,1714,1715],{"class":381},"# explicit copy — do not edit in place\n",[291,1717,1718],{"class":145,"line":367},[291,1719,314],{"emptyLinePlaceholder":313},[291,1721,1722,1724,1727,1729,1732],{"class":145,"line":372},[291,1723,470],{"class":296},[291,1725,1726],{"class":300}," index, vertex ",[291,1728,476],{"class":296},[291,1730,1731],{"class":336}," enumerate",[291,1733,1734],{"class":300},"(geometry.vertices()):\n",[291,1736,1737,1739,1742,1745,1748,1751,1754,1756,1759,1761],{"class":145,"line":385},[291,1738,484],{"class":336},[291,1740,1741],{"class":300},"(index, ",[291,1743,1744],{"class":336},"round",[291,1746,1747],{"class":300},"(vertex.x(), ",[291,1749,1750],{"class":336},"2",[291,1752,1753],{"class":300},"), ",[291,1755,1744],{"class":336},[291,1757,1758],{"class":300},"(vertex.y(), ",[291,1760,1750],{"class":336},[291,1762,1763],{"class":300},"))\n",[291,1765,1766],{"class":145,"line":396},[291,1767,314],{"emptyLinePlaceholder":313},[291,1769,1770,1773,1775,1777,1779,1782,1784,1786,1788,1791,1793],{"class":145,"line":722},[291,1771,1772],{"class":300},"geometry.moveVertex(QgsPointXY(",[291,1774,60],{"class":336},[291,1776,673],{"class":300},[291,1778,60],{"class":336},[291,1780,1781],{"class":300},").x(), QgsPointXY(",[291,1783,60],{"class":336},[291,1785,673],{"class":300},[291,1787,60],{"class":336},[291,1789,1790],{"class":300},").y(), ",[291,1792,60],{"class":336},[291,1794,1543],{"class":300},[291,1796,1797,1800,1803],{"class":145,"line":727},[291,1798,1799],{"class":300},"geometry.deleteVertex(",[291,1801,1802],{"class":336},"1",[291,1804,1543],{"class":300},[291,1806,1807,1809,1812,1816,1818,1820],{"class":145,"line":1123},[291,1808,375],{"class":336},[291,1810,1811],{"class":300},"(geometry.asWkt(",[291,1813,1815],{"class":1814},"s9osk","precision",[291,1817,323],{"class":296},[291,1819,1750],{"class":336},[291,1821,1763],{"class":300},[14,1823,1824,410,1826,1829,1830,1833,1834,1837,1838,431,1841,1844,1845,1848,1849,1851,1852,1855],{},[220,1825,409],{},[18,1827,1828],{},"QgsGeometry(other)"," makes the copy explicit; without it you are mutating an object that may be shared. ",[18,1831,1832],{},"vertices()"," yields ",[18,1835,1836],{},"QgsPoint"," objects in traversal order, and the index it produces is the same one ",[18,1839,1840],{},"moveVertex()",[18,1842,1843],{},"deleteVertex()"," expect. ",[18,1846,1847],{},"asWkt(precision=2)"," is the fastest way to eyeball a geometry in the console — full-precision WKT is unreadable. Bear in mind that ",[18,1850,1843],{}," can invalidate a geometry (a triangle with a vertex removed is a line), so re-check ",[18,1853,1854],{},"isGeosValid()"," before writing the result back.",[209,1857,1859],{"id":1858},"measuring-distance-and-area-correctly","Measuring distance and area correctly",[14,1861,1862,431,1865,1868,1869,1872],{},[18,1863,1864],{},"geometry.area()",[18,1866,1867],{},"geometry.distance(other)"," are ",[220,1870,1871],{},"planar"," measurements in the layer's own map units. In a projected CRS that is what you want. In EPSG:4326 they return square degrees and degrees, which are not units of area or length and vary with latitude.",[14,1874,1875,1876,1879],{},"For measurements that account for the curvature of the Earth, use ",[18,1877,1878],{},"QgsDistanceArea",", which performs ellipsoidal calculations:",[282,1881,1883],{"className":284,"code":1882,"language":286,"meta":287,"style":287},"from qgis.core import QgsDistanceArea, QgsProject, QgsUnitTypes\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\ncalculator = QgsDistanceArea()\ncalculator.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())\ncalculator.setEllipsoid(QgsProject.instance().ellipsoid())\n\nfor feature in layer.getFeatures():\n    square_metres = calculator.measureArea(feature.geometry())\n    hectares = calculator.convertAreaMeasurement(\n        square_metres, QgsUnitTypes.AreaUnit.AreaHectares\n    )\n    print(feature[\"parcel_id\"], round(hectares, 3))\n",[18,1884,1885,1896,1900,1916,1920,1930,1935,1940,1944,1956,1966,1976,1981,1986],{"__ignoreMap":287},[291,1886,1887,1889,1891,1893],{"class":145,"line":293},[291,1888,297],{"class":296},[291,1890,301],{"class":300},[291,1892,304],{"class":296},[291,1894,1895],{"class":300}," QgsDistanceArea, QgsProject, QgsUnitTypes\n",[291,1897,1898],{"class":145,"line":310},[291,1899,314],{"emptyLinePlaceholder":313},[291,1901,1902,1904,1906,1908,1910,1912,1914],{"class":145,"line":317},[291,1903,320],{"class":300},[291,1905,323],{"class":296},[291,1907,326],{"class":300},[291,1909,330],{"class":329},[291,1911,333],{"class":300},[291,1913,60],{"class":336},[291,1915,339],{"class":300},[291,1917,1918],{"class":145,"line":342},[291,1919,314],{"emptyLinePlaceholder":313},[291,1921,1922,1925,1927],{"class":145,"line":356},[291,1923,1924],{"class":300},"calculator ",[291,1926,323],{"class":296},[291,1928,1929],{"class":300}," QgsDistanceArea()\n",[291,1931,1932],{"class":145,"line":367},[291,1933,1934],{"class":300},"calculator.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())\n",[291,1936,1937],{"class":145,"line":372},[291,1938,1939],{"class":300},"calculator.setEllipsoid(QgsProject.instance().ellipsoid())\n",[291,1941,1942],{"class":145,"line":385},[291,1943,314],{"emptyLinePlaceholder":313},[291,1945,1946,1948,1951,1953],{"class":145,"line":396},[291,1947,470],{"class":296},[291,1949,1950],{"class":300}," feature ",[291,1952,476],{"class":296},[291,1954,1955],{"class":300}," layer.getFeatures():\n",[291,1957,1958,1961,1963],{"class":145,"line":722},[291,1959,1960],{"class":300},"    square_metres ",[291,1962,323],{"class":296},[291,1964,1965],{"class":300}," calculator.measureArea(feature.geometry())\n",[291,1967,1968,1971,1973],{"class":145,"line":727},[291,1969,1970],{"class":300},"    hectares ",[291,1972,323],{"class":296},[291,1974,1975],{"class":300}," calculator.convertAreaMeasurement(\n",[291,1977,1978],{"class":145,"line":1123},[291,1979,1980],{"class":300},"        square_metres, QgsUnitTypes.AreaUnit.AreaHectares\n",[291,1982,1983],{"class":145,"line":1132},[291,1984,1985],{"class":300},"    )\n",[291,1987,1988,1990,1993,1995,1998,2000,2003,2006],{"class":145,"line":1142},[291,1989,484],{"class":336},[291,1991,1992],{"class":300},"(feature[",[291,1994,1165],{"class":329},[291,1996,1997],{"class":300},"], ",[291,1999,1744],{"class":336},[291,2001,2002],{"class":300},"(hectares, ",[291,2004,2005],{"class":336},"3",[291,2007,1763],{"class":300},[14,2009,2010,410,2012,2015,2016,2019,2020,2023,2024,2027,2028,2031,2032,2036,2037,2041],{},[220,2011,409],{},[18,2013,2014],{},"setSourceCrs()"," tells the calculator what the incoming coordinates mean, and ",[18,2017,2018],{},"setEllipsoid()"," switches it from planar to ellipsoidal mode — without that second call it silently falls back to planar arithmetic. ",[18,2021,2022],{},"measureArea()"," returns square metres regardless of the source CRS, and ",[18,2025,2026],{},"convertAreaMeasurement()"," handles the unit conversion so you never hard-code 10000. This is exactly the machinery behind the ",[18,2029,2030],{},"$area"," expression variable discussed in ",[26,2033,2035],{"href":2034},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions",", and it is what ",[26,2038,2040],{"href":2039},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fpyqgis-script-to-calculate-polygon-areas\u002F","a script to calculate polygon areas"," uses under the hood.",[209,2043,2045],{"id":2044},"making-predicate-queries-fast","Making predicate queries fast",[14,2047,2048,2049,2052],{},"The naive spatial join — for every feature in A, test every feature in B — is quadratic. On two layers of 10,000 features that is 100 million predicate evaluations, each involving real geometric work. A ",[18,2050,2051],{},"QgsSpatialIndex"," reduces it to a bounding-box lookup followed by a handful of exact tests.",[14,2054,2055],{},[43,2056,2059,2062,2065,2068,2071,2074,2079,2120,2154,2156,2159,2162,2166,2173,2203,2212,2221,2223],{"viewBox":2057,"role":46,"ariaLabel":2058,"xmlns":48},"0 0 760 250","Comparison of a brute-force nested loop testing every feature pair against a spatial index that narrows candidates by bounding box before running exact predicates",[50,2060,2061],{},"Brute force versus a spatial index",[54,2063,2064],{},"On the left, a nested loop draws lines from one query shape to all sixteen candidate features, labelled as ten thousand by ten thousand exact tests. On the right, a spatial index bounding box selects three nearby candidates, and only those three receive an exact predicate test.",[58,2066],{"x":60,"y":60,"width":61,"height":2067,"fill":63},"250",[82,2069,2070],{"x":84,"y":92,"style":86,"fill":87,"textAnchor":88},"The index does not answer the question — it shortens the candidate list",[58,2072],{"x":529,"y":827,"width":2073,"height":532,"rx":96,"fill":97,"stroke":925,"style":99},"356",[82,2075,2078],{"x":2076,"y":2077,"style":104,"fill":925,"textAnchor":88},"194","64","nested loop — every pair",[525,2080,2083,2085,2088,2090,2093,2096,2099,2101,2103,2105,2107,2110,2112,2114,2116,2118],{"stroke":925,"style":2081,"opacity":2082},"stroke-width:1","0.5",[145,2084],{"x1":2076,"y1":94,"x2":834,"y2":829},[145,2086],{"x1":2076,"y1":94,"x2":2087,"y2":829},"110",[145,2089],{"x1":2076,"y1":94,"x2":563,"y2":829},[145,2091],{"x1":2076,"y1":94,"x2":2092,"y2":829},"228",[145,2094],{"x1":2076,"y1":94,"x2":2095,"y2":829},"278",[145,2097],{"x1":2076,"y1":94,"x2":2098,"y2":829},"328",[145,2100],{"x1":2076,"y1":94,"x2":834,"y2":94},[145,2102],{"x1":2076,"y1":94,"x2":2087,"y2":94},[145,2104],{"x1":2076,"y1":94,"x2":2095,"y2":94},[145,2106],{"x1":2076,"y1":94,"x2":2098,"y2":94},[145,2108],{"x1":2076,"y1":94,"x2":834,"y2":2109},"204",[145,2111],{"x1":2076,"y1":94,"x2":2087,"y2":2109},[145,2113],{"x1":2076,"y1":94,"x2":563,"y2":2109},[145,2115],{"x1":2076,"y1":94,"x2":2092,"y2":2109},[145,2117],{"x1":2076,"y1":94,"x2":2095,"y2":2109},[145,2119],{"x1":2076,"y1":94,"x2":2098,"y2":2109},[525,2121,2122,2124,2126,2128,2130,2132,2134,2136,2138,2140,2142,2144,2146,2148,2150,2152],{"fill":598},[172,2123],{"cx":834,"cy":829,"r":73},[172,2125],{"cx":2087,"cy":829,"r":73},[172,2127],{"cx":563,"cy":829,"r":73},[172,2129],{"cx":2092,"cy":829,"r":73},[172,2131],{"cx":2095,"cy":829,"r":73},[172,2133],{"cx":2098,"cy":829,"r":73},[172,2135],{"cx":834,"cy":94,"r":73},[172,2137],{"cx":2087,"cy":94,"r":73},[172,2139],{"cx":2095,"cy":94,"r":73},[172,2141],{"cx":2098,"cy":94,"r":73},[172,2143],{"cx":834,"cy":2109,"r":73},[172,2145],{"cx":2087,"cy":2109,"r":73},[172,2147],{"cx":563,"cy":2109,"r":73},[172,2149],{"cx":2092,"cy":2109,"r":73},[172,2151],{"cx":2095,"cy":2109,"r":73},[172,2153],{"cx":2098,"cy":2109,"r":73},[172,2155],{"cx":2076,"cy":94,"r":72,"fill":98},[82,2157,2158],{"x":2076,"y":1231,"style":109,"fill":80,"textAnchor":88},"10 000 × 10 000 exact tests",[58,2160],{"x":2161,"y":827,"width":2073,"height":532,"rx":96,"fill":97,"stroke":163,"style":99},"388",[82,2163,2165],{"x":2164,"y":2077,"style":104,"fill":163,"textAnchor":88},"566","spatial index — candidates first",[58,2167],{"x":884,"y":2168,"width":2169,"height":103,"rx":2170,"fill":163,"fillOpacity":2171,"stroke":163,"style":2172},"112","124","4",0.12,"stroke-width:2;stroke-dasharray:6 4",[525,2174,2175,2178,2180,2183,2186,2188,2191,2193,2195,2197,2199,2201],{"fill":598},[172,2176],{"cx":2177,"cy":829,"r":73},"432",[172,2179],{"cx":879,"cy":829,"r":73},[172,2181],{"cx":2182,"cy":829,"r":73},"532",[172,2184],{"cx":2185,"cy":829,"r":73},"600",[172,2187],{"cx":899,"cy":829,"r":73},[172,2189],{"cx":2190,"cy":829,"r":73},"700",[172,2192],{"cx":2177,"cy":94,"r":73},[172,2194],{"cx":879,"cy":94,"r":73},[172,2196],{"cx":2190,"cy":94,"r":73},[172,2198],{"cx":2177,"cy":2109,"r":73},[172,2200],{"cx":879,"cy":2109,"r":73},[172,2202],{"cx":2190,"cy":2109,"r":73},[525,2204,2205,2208,2210],{"fill":163},[172,2206],{"cx":2182,"cy":94,"r":2207},"6",[172,2209],{"cx":2185,"cy":94,"r":2207},[172,2211],{"cx":2182,"cy":114,"r":2207},[525,2213,2215,2217,2219],{"stroke":163,"style":2214},"stroke-width:1.6",[145,2216],{"x1":2164,"y1":94,"x2":2182,"y2":94},[145,2218],{"x1":2164,"y1":94,"x2":2185,"y2":94},[145,2220],{"x1":2164,"y1":94,"x2":2182,"y2":114},[172,2222],{"cx":2164,"cy":94,"r":72,"fill":98},[82,2224,2225],{"x":2164,"y":1231,"style":109,"fill":80,"textAnchor":88},"3 exact tests after a box lookup",[282,2227,2229],{"className":284,"code":2228,"language":286,"meta":287,"style":287},"from qgis.core import QgsProject, QgsSpatialIndex\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nzones = QgsProject.instance().mapLayersByName(\"flood_zones\")[0]\n\nindex = QgsSpatialIndex(zones.getFeatures())\nzone_geometries = {f.id(): f.geometry() for f in zones.getFeatures()}\n\nfor parcel in parcels.getFeatures():\n    geometry = parcel.geometry()\n    for zone_id in index.intersects(geometry.boundingBox()):\n        if geometry.intersects(zone_geometries[zone_id]):\n            print(parcel[\"parcel_id\"], \"hits zone\", zone_id)\n            break\n",[18,2230,2231,2242,2246,2262,2278,2282,2292,2311,2315,2325,2333,2346,2354,2371],{"__ignoreMap":287},[291,2232,2233,2235,2237,2239],{"class":145,"line":293},[291,2234,297],{"class":296},[291,2236,301],{"class":300},[291,2238,304],{"class":296},[291,2240,2241],{"class":300}," QgsProject, QgsSpatialIndex\n",[291,2243,2244],{"class":145,"line":310},[291,2245,314],{"emptyLinePlaceholder":313},[291,2247,2248,2250,2252,2254,2256,2258,2260],{"class":145,"line":317},[291,2249,1030],{"class":300},[291,2251,323],{"class":296},[291,2253,326],{"class":300},[291,2255,330],{"class":329},[291,2257,333],{"class":300},[291,2259,60],{"class":336},[291,2261,339],{"class":300},[291,2263,2264,2266,2268,2270,2272,2274,2276],{"class":145,"line":342},[291,2265,1047],{"class":300},[291,2267,323],{"class":296},[291,2269,326],{"class":300},[291,2271,1054],{"class":329},[291,2273,333],{"class":300},[291,2275,60],{"class":336},[291,2277,339],{"class":300},[291,2279,2280],{"class":145,"line":356},[291,2281,314],{"emptyLinePlaceholder":313},[291,2283,2284,2287,2289],{"class":145,"line":367},[291,2285,2286],{"class":300},"index ",[291,2288,323],{"class":296},[291,2290,2291],{"class":300}," QgsSpatialIndex(zones.getFeatures())\n",[291,2293,2294,2297,2299,2302,2304,2306,2308],{"class":145,"line":372},[291,2295,2296],{"class":300},"zone_geometries ",[291,2298,323],{"class":296},[291,2300,2301],{"class":300}," {f.id(): f.geometry() ",[291,2303,470],{"class":296},[291,2305,1588],{"class":300},[291,2307,476],{"class":296},[291,2309,2310],{"class":300}," zones.getFeatures()}\n",[291,2312,2313],{"class":145,"line":385},[291,2314,314],{"emptyLinePlaceholder":313},[291,2316,2317,2319,2321,2323],{"class":145,"line":396},[291,2318,470],{"class":296},[291,2320,1087],{"class":300},[291,2322,476],{"class":296},[291,2324,1092],{"class":300},[291,2326,2327,2329,2331],{"class":145,"line":722},[291,2328,1097],{"class":300},[291,2330,323],{"class":296},[291,2332,1102],{"class":300},[291,2334,2335,2338,2341,2343],{"class":145,"line":727},[291,2336,2337],{"class":296},"    for",[291,2339,2340],{"class":300}," zone_id ",[291,2342,476],{"class":296},[291,2344,2345],{"class":300}," index.intersects(geometry.boundingBox()):\n",[291,2347,2348,2351],{"class":145,"line":1123},[291,2349,2350],{"class":296},"        if",[291,2352,2353],{"class":300}," geometry.intersects(zone_geometries[zone_id]):\n",[291,2355,2356,2359,2361,2363,2365,2368],{"class":145,"line":1132},[291,2357,2358],{"class":336},"            print",[291,2360,1162],{"class":300},[291,2362,1165],{"class":329},[291,2364,1997],{"class":300},[291,2366,2367],{"class":329},"\"hits zone\"",[291,2369,2370],{"class":300},", zone_id)\n",[291,2372,2373],{"class":145,"line":1142},[291,2374,2375],{"class":296},"            break\n",[14,2377,2378,410,2380,2383,2384,2387,2388,2391,2392,2395,2396,431,2399,2402,2403,2407],{},[220,2379,409],{},[18,2381,2382],{},"QgsSpatialIndex(zones.getFeatures())"," builds an R-tree over the bounding boxes in one pass. ",[18,2385,2386],{},"index.intersects(bbox)"," returns candidate feature IDs whose ",[742,2389,2390],{},"boxes"," overlap — a fast approximation that can include false positives, which is why the exact ",[18,2393,2394],{},"geometry.intersects()"," test still runs. Caching the geometries in a dictionary avoids a second provider round-trip per candidate; on very large layers, prefer ",[18,2397,2398],{},"QgsSpatialIndex(..., flags=QgsSpatialIndex.FlagStoreFeatureGeometries)",[18,2400,2401],{},"index.geometry(id)"," so the geometries live in the index itself. ",[26,2404,2406],{"href":2405},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build and Use a Spatial Index in PyQGIS"," covers the memory trade-off and nearest-neighbour queries.",[209,2409,2411],{"id":2410},"validity-why-operations-fail","Validity: why operations fail",[14,2413,2414,2415,673,2417,2419,2420,2422],{},"GEOS, the library behind these operations, requires valid geometry. A self-intersecting polygon, a ring that touches itself, or a polygon whose hole falls outside its shell will make ",[18,2416,739],{},[18,2418,1205],{},", or ",[18,2421,1212],{}," raise or return an empty result — often with a terse message that names a coordinate rather than a feature.",[282,2424,2426],{"className":284,"code":2425,"language":286,"meta":287,"style":287},"from qgis.core import QgsGeometry, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nfor feature in layer.getFeatures():\n    geometry = feature.geometry()\n    errors = geometry.validateGeometry()\n    if errors:\n        print(feature.id(), errors[0].what())\n        repaired = geometry.makeValid()\n        print(\"  repaired:\", repaired.isGeosValid())\n",[18,2427,2428,2438,2442,2458,2462,2472,2480,2490,2497,2510,2520],{"__ignoreMap":287},[291,2429,2430,2432,2434,2436],{"class":145,"line":293},[291,2431,297],{"class":296},[291,2433,301],{"class":300},[291,2435,304],{"class":296},[291,2437,1345],{"class":300},[291,2439,2440],{"class":145,"line":310},[291,2441,314],{"emptyLinePlaceholder":313},[291,2443,2444,2446,2448,2450,2452,2454,2456],{"class":145,"line":317},[291,2445,320],{"class":300},[291,2447,323],{"class":296},[291,2449,326],{"class":300},[291,2451,330],{"class":329},[291,2453,333],{"class":300},[291,2455,60],{"class":336},[291,2457,339],{"class":300},[291,2459,2460],{"class":145,"line":342},[291,2461,314],{"emptyLinePlaceholder":313},[291,2463,2464,2466,2468,2470],{"class":145,"line":356},[291,2465,470],{"class":296},[291,2467,1950],{"class":300},[291,2469,476],{"class":296},[291,2471,1955],{"class":300},[291,2473,2474,2476,2478],{"class":145,"line":367},[291,2475,1097],{"class":300},[291,2477,323],{"class":296},[291,2479,364],{"class":300},[291,2481,2482,2485,2487],{"class":145,"line":372},[291,2483,2484],{"class":300},"    errors ",[291,2486,323],{"class":296},[291,2488,2489],{"class":300}," geometry.validateGeometry()\n",[291,2491,2492,2494],{"class":145,"line":385},[291,2493,1107],{"class":296},[291,2495,2496],{"class":300}," errors:\n",[291,2498,2499,2502,2505,2507],{"class":145,"line":396},[291,2500,2501],{"class":336},"        print",[291,2503,2504],{"class":300},"(feature.id(), errors[",[291,2506,60],{"class":336},[291,2508,2509],{"class":300},"].what())\n",[291,2511,2512,2515,2517],{"class":145,"line":722},[291,2513,2514],{"class":300},"        repaired ",[291,2516,323],{"class":296},[291,2518,2519],{"class":300}," geometry.makeValid()\n",[291,2521,2522,2524,2526,2529],{"class":145,"line":727},[291,2523,2501],{"class":336},[291,2525,1484],{"class":300},[291,2527,2528],{"class":329},"\"  repaired:\"",[291,2530,2531],{"class":300},", repaired.isGeosValid())\n",[14,2533,2534,410,2536,2539,2540,2543,2544,2547,2548,2550,2551,2553,2554,233],{},[220,2535,409],{},[18,2537,2538],{},"validateGeometry()"," returns a list of ",[18,2541,2542],{},"QgsGeometry.Error"," objects, each with a message and often a location — far more actionable than a boolean. ",[18,2545,2546],{},"makeValid()"," returns a repaired copy using the GEOS validity routines; it can change the geometry type (a self-intersecting polygon may become a multi-polygon), so check the result before writing it back. ",[18,2549,1854],{}," is the cheap boolean check when you do not need the details. The full repair workflow, including what to do when ",[18,2552,2546],{}," changes the part count, is in ",[26,2555,2557],{"href":2556},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Ffix-invalid-geometries-pyqgis\u002F","Find and Fix Invalid Geometries in PyQGIS",[209,2559,2561],{"id":2560},"reading-and-writing-geometry-as-text","Reading and writing geometry as text",[14,2563,2564,2565,2567],{},"Debugging geometry problems is much easier when you can see the coordinates, and interoperating with other tools usually means exchanging a text or binary encoding rather than a QGIS object. ",[18,2566,20],{}," converts in both directions.",[282,2569,2571],{"className":284,"code":2570,"language":286,"meta":287,"style":287},"from qgis.core import QgsGeometry\n\nwkt = \"POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))\"\ngeometry = QgsGeometry.fromWkt(wkt)\nif geometry.isNull():\n    raise ValueError(\"could not parse that WKT\")\n\nprint(geometry.asWkt(precision=1))\nprint(geometry.asJson(precision=6))          # GeoJSON geometry object\nprint(len(geometry.asWkb()))                 # compact binary form, in bytes\n",[18,2572,2573,2584,2588,2598,2607,2614,2629,2633,2647,2666],{"__ignoreMap":287},[291,2574,2575,2577,2579,2581],{"class":145,"line":293},[291,2576,297],{"class":296},[291,2578,301],{"class":300},[291,2580,304],{"class":296},[291,2582,2583],{"class":300}," QgsGeometry\n",[291,2585,2586],{"class":145,"line":310},[291,2587,314],{"emptyLinePlaceholder":313},[291,2589,2590,2593,2595],{"class":145,"line":317},[291,2591,2592],{"class":300},"wkt ",[291,2594,323],{"class":296},[291,2596,2597],{"class":329}," \"POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))\"\n",[291,2599,2600,2602,2604],{"class":145,"line":342},[291,2601,359],{"class":300},[291,2603,323],{"class":296},[291,2605,2606],{"class":300}," QgsGeometry.fromWkt(wkt)\n",[291,2608,2609,2611],{"class":145,"line":356},[291,2610,456],{"class":296},[291,2612,2613],{"class":300}," geometry.isNull():\n",[291,2615,2616,2619,2622,2624,2627],{"class":145,"line":367},[291,2617,2618],{"class":296},"    raise",[291,2620,2621],{"class":336}," ValueError",[291,2623,1484],{"class":300},[291,2625,2626],{"class":329},"\"could not parse that WKT\"",[291,2628,1543],{"class":300},[291,2630,2631],{"class":145,"line":372},[291,2632,314],{"emptyLinePlaceholder":313},[291,2634,2635,2637,2639,2641,2643,2645],{"class":145,"line":385},[291,2636,375],{"class":336},[291,2638,1811],{"class":300},[291,2640,1815],{"class":1814},[291,2642,323],{"class":296},[291,2644,1802],{"class":336},[291,2646,1763],{"class":300},[291,2648,2649,2651,2654,2656,2658,2660,2663],{"class":145,"line":396},[291,2650,375],{"class":336},[291,2652,2653],{"class":300},"(geometry.asJson(",[291,2655,1815],{"class":1814},[291,2657,323],{"class":296},[291,2659,2207],{"class":336},[291,2661,2662],{"class":300},"))          ",[291,2664,2665],{"class":381},"# GeoJSON geometry object\n",[291,2667,2668,2670,2672,2674,2677],{"class":145,"line":722},[291,2669,375],{"class":336},[291,2671,1484],{"class":300},[291,2673,1618],{"class":336},[291,2675,2676],{"class":300},"(geometry.asWkb()))                 ",[291,2678,2679],{"class":381},"# compact binary form, in bytes\n",[14,2681,2682,410,2684,2687,2688,2691,2692,2695,2696,2699,2700,2703,2704,2707],{},[220,2683,409],{},[18,2685,2686],{},"fromWkt()"," returns a ",[742,2689,2690],{},"null geometry"," rather than raising when the text is malformed, so ",[18,2693,2694],{},"isNull()"," is the required check — a null geometry passed downstream produces confusing empty results much later. ",[18,2697,2698],{},"asWkt(precision=...)"," rounds the output, which is what makes it readable in a console or a test assertion. ",[18,2701,2702],{},"asJson()"," emits the GeoJSON geometry fragment, ready to embed in a feature; note it always assumes the coordinates are already in the CRS you intend to publish, so reproject first. ",[18,2705,2706],{},"asWkb()"," is the compact binary encoding used for database round-trips.",[14,2709,2710,2711,2714,2715,233],{},"This is also the most practical way to write a regression test for a geometry routine: build the input from WKT, run the operation, and compare ",[18,2712,2713],{},"asWkt(precision=3)"," against an expected string. That avoids depending on a data file and makes failures readable. The same pattern underpins the plugin test suites described in ",[26,2716,2718],{"href":2717},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","Testing and CI for QGIS Plugins",[14,2720,2721],{},"One conversion trap is worth naming: WKT carries no CRS. A polygon read from WKT is just numbers, and QGIS will happily treat it as being in whatever CRS you attach it to. When a geometry arrives from an external system, confirm the CRS out of band and reproject explicitly rather than assuming it matches the layer you are about to compare it against.",[209,2723,2725],{"id":2724},"key-takeaways","Key takeaways",[214,2727,2728,2739,2748,2754,2760,2769,2778],{},[217,2729,2730,2735,2736,2738],{},[220,2731,2732,2734],{},[18,2733,20],{}," is a value-type handle."," Copying one is cheap and mutating it never edits the source layer; ",[18,2737,143],{}," reaches the coordinates when you need vertex-level access.",[217,2740,2741,2744,2745,2747],{},[220,2742,2743],{},"Normalise single versus multi-part early."," Iterating ",[18,2746,494],{}," with a one-element fallback removes an entire class of \"only the first polygon was processed\" bugs.",[217,2749,2750,2753],{},[220,2751,2752],{},"Buffer and simplify distances are in map units."," In a geographic CRS that means degrees, so reproject to a metric CRS before measuring anything.",[217,2755,2756,2759],{},[220,2757,2758],{},"Touching intersects but does not overlap, and every geometry is within itself."," Those two facts explain most spatial-join surprises.",[217,2761,2762,2768],{},[220,2763,2764,2765,2767],{},"Use ",[18,2766,1878],{}," for real-world measurements."," Set both the source CRS and the ellipsoid; without the ellipsoid it quietly stays planar.",[217,2770,2771,2774,2775,2777],{},[220,2772,2773],{},"Index before joining."," A ",[18,2776,2051],{}," turns a quadratic scan into a bounding-box lookup plus a few exact tests, and the exact test is still required.",[217,2779,2780,410,2783,2785,2786,2788],{},[220,2781,2782],{},"Validate before operating.",[18,2784,2538],{}," reports what is wrong and where; ",[18,2787,2546],{}," repairs it but may change the geometry type.",[209,2790,2792],{"id":2791},"frequently-asked-questions","Frequently Asked Questions",[14,2794,2795,2798,2799,2802,2803,2805],{},[220,2796,2797],{},"Why does buffer() produce a strange shape or an empty geometry?","\nAlmost always invalid input or the wrong CRS. Run ",[18,2800,2801],{},"geometry.isGeosValid()"," first and repair with ",[18,2804,2546],{}," if it fails. If the geometry is valid but the buffer looks enormous or microscopic, the layer is probably in a geographic CRS and the distance is being read as degrees.",[14,2807,2808,2811,2813,2814,2816],{},[220,2809,2810],{},"What is the difference between intersects() and overlaps()?",[18,2812,781],{}," is true whenever the geometries share any point at all, including a single boundary touch. ",[18,2815,796],{}," additionally requires that their interiors share area and that neither one contains the other. Two parcels sharing an edge intersect but do not overlap.",[14,2818,2819,2822,2824],{},[220,2820,2821],{},"Should I use centroid() or pointOnSurface() for labelling?",[18,2823,758],{},", because it is guaranteed to fall inside the geometry. The centroid of a concave or ring-shaped polygon can land outside it entirely, which puts the label in the wrong place or over a neighbour.",[14,2826,2827,2830,2831,2833,2834,2836,2837,2839,2840,2843,2844,2846],{},[220,2828,2829],{},"How do I get area in hectares instead of square map units?","\nUse ",[18,2832,1878],{}," with the source CRS and project ellipsoid set, call ",[18,2835,2022],{}," for square metres, then ",[18,2838,2026],{}," with ",[18,2841,2842],{},"AreaUnit.AreaHectares",". Calling ",[18,2845,1864],{}," directly returns square map units, which are square degrees in EPSG:4326.",[14,2848,2849,2852],{},[220,2850,2851],{},"Does a spatial index give me the answer directly?","\nNo. It returns candidate feature IDs whose bounding boxes intersect your query box, which is an approximation that includes false positives. You still run the exact predicate on each candidate — the index only shortens the list.",[209,2854,2856],{"id":2855},"related","Related",[214,2858,2859,2865,2870,2874,2880,2884,2888,2892,2898,2902],{},[217,2860,2861,2864],{},[26,2862,2863],{"href":28},"Spatial Data Processing & Automation with PyQGIS"," — the guide this page belongs to",[217,2866,2867],{},[26,2868,2869],{"href":33},"Vector Data Manipulation in PyQGIS",[217,2871,2872],{},[26,2873,244],{"href":243},[217,2875,2876],{},[26,2877,2879],{"href":2878},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS",[217,2881,2882],{},[26,2883,763],{"href":762},[217,2885,2886],{},[26,2887,1186],{"href":1185},[217,2889,2890],{},[26,2891,2557],{"href":2556},[217,2893,2894],{},[26,2895,2897],{"href":2896},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcalculate-distance-between-features-pyqgis\u002F","Calculate the Distance Between Features in PyQGIS",[217,2899,2900],{},[26,2901,767],{"href":766},[217,2903,2904],{},[26,2905,2406],{"href":2405},[2907,2908,2909],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":287,"searchDepth":310,"depth":310,"links":2911},[2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924],{"id":211,"depth":310,"text":212},{"id":255,"depth":310,"text":256},{"id":498,"depth":310,"text":499},{"id":771,"depth":310,"text":772},{"id":1190,"depth":310,"text":1191},{"id":1649,"depth":310,"text":1650},{"id":1858,"depth":310,"text":1859},{"id":2044,"depth":310,"text":2045},{"id":2410,"depth":310,"text":2411},{"id":2560,"depth":310,"text":2561},{"id":2724,"depth":310,"text":2725},{"id":2791,"depth":310,"text":2792},{"id":2855,"depth":310,"text":2856},"Work with QgsGeometry in PyQGIS. Run constructive operations, test spatial predicates, measure distance and area correctly, and speed up queries with a spatial index.","md",{"slug":2928,"type":2929,"breadcrumb":2930,"datePublished":2931,"dateModified":2931},"geometry-operations-and-predicates","guide","Geometry Operations","2026-08-01","\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates",{"title":5,"description":2925},"spatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Findex","AMg2f92Y6cP74aH-JA-26cCkAgiB8PifBiSiotpEGVg",1785585990476]