[{"data":1,"prerenderedAt":2134},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fiterate-and-edit-geometry-vertices-pyqgis":3},{"id":4,"title":5,"body":6,"description":2123,"extension":2124,"meta":2125,"navigation":241,"path":2130,"seo":2131,"stem":2132,"__hash__":2133},"docs\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fiterate-and-edit-geometry-vertices-pyqgis\u002Findex.md","Iterate and Edit Geometry Vertices in PyQGIS",{"type":7,"value":8,"toc":2110},"minimark",[9,13,17,26,166,171,188,192,208,493,538,555,559,562,690,1213,1221,1225,1230,1426,1466,1470,1473,1583,1769,1790,1794,1797,1892,1922,1926,1954,1958,2003,2007,2016,2020,2034,2050,2066,2072,2076,2106],[10,11,5],"h1",{"id":12},"iterate-and-edit-geometry-vertices-in-pyqgis",[14,15,16],"p",{},"Most geometry work in PyQGIS happens at the level of whole shapes: buffer this, intersect that, simplify the result. Some jobs need to go underneath — to the individual coordinates. Finding polygons with more than 10,000 vertices that make a web service choke, spotting spikes where a digitiser double-clicked, rounding coordinates to the survey's actual precision, stripping Z values that a CAD export added, nudging one vertex that sits a centimetre off a boundary. All of those mean walking the vertices of a geometry and, sometimes, changing them.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations and Spatial Predicates in PyQGIS",". It explains how geometries nest, reads vertices with their positions in that structure, edits them in place, cleans coordinates in bulk, and writes the result back to the layer.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,80,85,92,99,104,108,117,123,128,134,141,147,151,154,157,161],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 310","img","How a multipolygon nests: a geometry holds parts, each polygon part holds an exterior ring and interior rings, each ring holds vertices, and a vertex id records part, ring and vertex index","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Parts, rings, vertices",[40,41,42],"desc",{},"A MultiPolygon geometry contains two polygon parts. Part 0 has an exterior ring and one interior ring, a hole. Part 1 has only an exterior ring. Each ring is a closed list of vertices whose first and last coordinates are equal. A QgsVertexId addresses one vertex by part, ring and vertex index, for example part 0, ring 1, vertex 3 for a vertex of the hole.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","310","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A vertex is addressed by part, ring and index",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","48","330","240","10","#fffdf7","#59645f","stroke-width:2",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M60 100 L200 80 L230 180 L150 250 L70 220 z","#e8efe6","#15803d","stroke-width:2.5",[70,77],{"d":78,"fill":66,"stroke":79,"style":68},"M110 150 L165 140 L175 190 L120 200 z","#b45309",[70,81],{"d":82,"fill":83,"stroke":84,"style":75},"M260 110 L330 120 L320 190 L270 180 z","#eff3ff","#2563eb",[86,87],"circle",{"cx":88,"cy":89,"r":90,"fill":91},"175","190","6","#b91c1c",[51,93,98],{"x":94,"y":95,"style":96,"fill":97,"textAnchor":57},"150","276","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","part 0 (with hole)",[51,100,103],{"x":101,"y":102,"style":96,"fill":97,"textAnchor":57},"296","210","part 1",[44,105],{"x":106,"y":62,"width":107,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"378","358",[44,109],{"x":110,"y":111,"width":112,"height":113,"rx":90,"fill":114,"stroke":115,"style":116},"398","64","318","30","#eef7f4","#0f766e","stroke-width:1.5",[51,118,122],{"x":119,"y":120,"style":121,"fill":97,"textAnchor":57},"557","84","text-anchor:middle;font-size:10.5px;font-family:monospace","MultiPolygon",[44,124],{"x":125,"y":126,"width":127,"height":54,"rx":90,"fill":73,"stroke":74,"style":116},"418","104","298",[51,129,133],{"x":130,"y":131,"style":132,"fill":97,"textAnchor":57},"567","123","text-anchor:middle;font-size:10px;font-family:monospace","part 0: Polygon",[44,135],{"x":136,"y":137,"width":138,"height":139,"rx":90,"fill":66,"stroke":74,"style":140},"438","140","278","26","stroke-width:1.2",[51,142,146],{"x":143,"y":144,"style":145,"fill":97,"textAnchor":57},"577","158","text-anchor:middle;font-size:9.5px;font-family:monospace","ring 0: exterior, 6 vertices",[44,148],{"x":136,"y":149,"width":138,"height":139,"rx":90,"fill":150,"stroke":79,"style":140},"172","#fdf2e2",[51,152,153],{"x":143,"y":89,"style":145,"fill":97,"textAnchor":57},"ring 1: hole, 5 vertices",[44,155],{"x":125,"y":156,"width":127,"height":54,"rx":90,"fill":83,"stroke":84,"style":116},"208",[51,158,160],{"x":130,"y":159,"style":132,"fill":97,"textAnchor":57},"227","part 1: Polygon, 1 ring",[51,162,165],{"x":119,"y":163,"style":164,"fill":91,"textAnchor":57},"266","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","red vertex = part 0, ring 1, vertex 2",[167,168,170],"h2",{"id":169},"prerequisites","Prerequisites",[172,173,174,182,185],"ul",{},[175,176,177,181],"li",{},[178,179,180],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[175,183,184],{},"An editable vector layer for the editing sections; reading works on any layer.",[175,186,187],{},"Awareness that editing coordinates changes the data permanently once committed. Work on a copy until the script is proven.",[167,189,191],{"id":190},"read-every-vertex-with-its-position","Read every vertex with its position",[14,193,194,198,199,202,203,207],{},[195,196,197],"code",{},"QgsGeometry.vertices()"," iterates every vertex as a ",[195,200,201],{},"QgsPoint",", flattening parts and rings. That is enough for counting and bounding checks. When you need to know ",[204,205,206],"em",{},"where"," in the structure a vertex sits — which part, which ring — walk the vertex ids alongside.",[209,210,215],"pre",{"className":211,"code":212,"language":213,"meta":214,"style":214},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nfeature = next(parcels.getFeatures())\ngeom = feature.geometry()\n\nprint(\"vertices:\", sum(1 for _ in geom.vertices()), \"parts:\", geom.constGet().partCount())\n\nfor flat_index in range(geom.constGet().nCoordinates()):\n    ok, vid = geom.vertexIdFromVertexNr(flat_index)\n    if not ok:\n        break\n    point = geom.vertexAt(flat_index)\n    print(f\"part {vid.part} ring {vid.ring} vertex {vid.vertex} \"\n          f\"(#{flat_index}): {point.x():.2f}, {point.y():.2f}\")\n","python","",[195,216,217,236,243,268,282,293,298,339,344,361,372,384,390,401,447],{"__ignoreMap":214},[218,219,222,226,230,233],"span",{"class":220,"line":221},"line",1,[218,223,225],{"class":224},"snl16","from",[218,227,229],{"class":228},"s95oV"," qgis.core ",[218,231,232],{"class":224},"import",[218,234,235],{"class":228}," QgsProject\n",[218,237,239],{"class":220,"line":238},2,[218,240,242],{"emptyLinePlaceholder":241},true,"\n",[218,244,246,249,252,255,259,262,265],{"class":220,"line":245},3,[218,247,248],{"class":228},"parcels ",[218,250,251],{"class":224},"=",[218,253,254],{"class":228}," QgsProject.instance().mapLayersByName(",[218,256,258],{"class":257},"sU2Wk","\"parcels\"",[218,260,261],{"class":228},")[",[218,263,46],{"class":264},"sDLfK",[218,266,267],{"class":228},"]\n",[218,269,271,274,276,279],{"class":220,"line":270},4,[218,272,273],{"class":228},"feature ",[218,275,251],{"class":224},[218,277,278],{"class":264}," next",[218,280,281],{"class":228},"(parcels.getFeatures())\n",[218,283,285,288,290],{"class":220,"line":284},5,[218,286,287],{"class":228},"geom ",[218,289,251],{"class":224},[218,291,292],{"class":228}," feature.geometry()\n",[218,294,296],{"class":220,"line":295},6,[218,297,242],{"emptyLinePlaceholder":241},[218,299,301,304,307,310,313,316,318,321,324,327,330,333,336],{"class":220,"line":300},7,[218,302,303],{"class":264},"print",[218,305,306],{"class":228},"(",[218,308,309],{"class":257},"\"vertices:\"",[218,311,312],{"class":228},", ",[218,314,315],{"class":264},"sum",[218,317,306],{"class":228},[218,319,320],{"class":264},"1",[218,322,323],{"class":224}," for",[218,325,326],{"class":228}," _ ",[218,328,329],{"class":224},"in",[218,331,332],{"class":228}," geom.vertices()), ",[218,334,335],{"class":257},"\"parts:\"",[218,337,338],{"class":228},", geom.constGet().partCount())\n",[218,340,342],{"class":220,"line":341},8,[218,343,242],{"emptyLinePlaceholder":241},[218,345,347,350,353,355,358],{"class":220,"line":346},9,[218,348,349],{"class":224},"for",[218,351,352],{"class":228}," flat_index ",[218,354,329],{"class":224},[218,356,357],{"class":264}," range",[218,359,360],{"class":228},"(geom.constGet().nCoordinates()):\n",[218,362,364,367,369],{"class":220,"line":363},10,[218,365,366],{"class":228},"    ok, vid ",[218,368,251],{"class":224},[218,370,371],{"class":228}," geom.vertexIdFromVertexNr(flat_index)\n",[218,373,375,378,381],{"class":220,"line":374},11,[218,376,377],{"class":224},"    if",[218,379,380],{"class":224}," not",[218,382,383],{"class":228}," ok:\n",[218,385,387],{"class":220,"line":386},12,[218,388,389],{"class":224},"        break\n",[218,391,393,396,398],{"class":220,"line":392},13,[218,394,395],{"class":228},"    point ",[218,397,251],{"class":224},[218,399,400],{"class":228}," geom.vertexAt(flat_index)\n",[218,402,404,407,409,412,415,418,421,424,427,429,432,434,437,439,442,444],{"class":220,"line":403},14,[218,405,406],{"class":264},"    print",[218,408,306],{"class":228},[218,410,411],{"class":224},"f",[218,413,414],{"class":257},"\"part ",[218,416,417],{"class":264},"{",[218,419,420],{"class":228},"vid.part",[218,422,423],{"class":264},"}",[218,425,426],{"class":257}," ring ",[218,428,417],{"class":264},[218,430,431],{"class":228},"vid.ring",[218,433,423],{"class":264},[218,435,436],{"class":257}," vertex ",[218,438,417],{"class":264},[218,440,441],{"class":228},"vid.vertex",[218,443,423],{"class":264},[218,445,446],{"class":257}," \"\n",[218,448,450,453,456,458,461,463,466,468,471,474,476,478,480,483,485,487,490],{"class":220,"line":449},15,[218,451,452],{"class":224},"          f",[218,454,455],{"class":257},"\"(#",[218,457,417],{"class":264},[218,459,460],{"class":228},"flat_index",[218,462,423],{"class":264},[218,464,465],{"class":257},"): ",[218,467,417],{"class":264},[218,469,470],{"class":228},"point.x()",[218,472,473],{"class":224},":.2f",[218,475,423],{"class":264},[218,477,312],{"class":257},[218,479,417],{"class":264},[218,481,482],{"class":228},"point.y()",[218,484,473],{"class":224},[218,486,423],{"class":264},[218,488,489],{"class":257},"\"",[218,491,492],{"class":228},")\n",[14,494,495,498,499,502,503,506,507,510,511,514,515,518,519,312,522,525,526,529,530,533,534,537],{},[178,496,497],{},"Breakdown:"," ",[195,500,501],{},"constGet()"," returns the underlying ",[195,504,505],{},"QgsAbstractGeometry"," without copying it, which is what exposes structural methods such as ",[195,508,509],{},"partCount"," and ",[195,512,513],{},"nCoordinates",". ",[195,516,517],{},"vertexIdFromVertexNr"," converts a flat vertex index — the kind ",[195,520,521],{},"vertexAt",[195,523,524],{},"moveVertex"," and the other ",[195,527,528],{},"QgsGeometry"," editing methods use — into a ",[195,531,532],{},"QgsVertexId"," holding part, ring and vertex numbers, and ",[195,535,536],{},"vertexNrFromVertexId"," goes the other way. Keeping both is how you move between \"the third vertex of the hole\" and \"vertex 14\". Rings are closed, so a square ring reports five vertices with the first repeated at the end; count distinct positions when a human-facing number matters.",[14,539,540,541,312,544,510,547,550,551,554],{},"For simple geometries the nested Python lists are often more convenient: ",[195,542,543],{},"asPolyline()",[195,545,546],{},"asPolygon()",[195,548,549],{},"asMultiPolygon()"," return lists of ",[195,552,553],{},"QgsPointXY",", nested one level per structure. They drop Z and M values, so use the vertex iterator when those matter.",[167,556,558],{"id":557},"find-problem-geometries-by-their-vertices","Find problem geometries by their vertices",[14,560,561],{},"Vertex-level checks catch problems that validity checks do not: a geometry can be perfectly valid and still have 40,000 vertices along a straight fence, or a spike that doubles back on itself.",[14,563,564],{},[29,565,568,571,574,577,580,585,591,596,648,652,655,658,662,666,669,672,675,679,682,687],{"viewBox":566,"role":32,"ariaLabel":567,"xmlns":34},"0 0 760 256","Three vertex-level problems a validity check misses: excessive vertex density along a straight edge, duplicate consecutive vertices, and a spike where the line goes out and returns along almost the same path",[36,569,570],{},"Valid, but still wrong",[40,572,573],{},"Left: a straight parcel edge carrying hundreds of vertices from a stream-digitised capture. Middle: two vertices at exactly the same coordinate, which inflate counts and confuse some tools. Right: a spike where a digitiser overshot and returned, creating an angle of a few degrees between consecutive segments.",[44,575],{"x":46,"y":46,"width":47,"height":576,"fill":49},"256",[51,578,579],{"x":53,"y":139,"style":55,"fill":56,"textAnchor":57},"Problems isValid() will not report",[44,581],{"x":61,"y":582,"width":583,"height":584,"rx":65,"fill":66,"stroke":79,"style":75},"44","222","192",[51,586,590],{"x":587,"y":588,"style":589,"fill":79,"textAnchor":57},"135","68","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","too dense",[220,592],{"x1":593,"y1":137,"x2":594,"y2":137,"stroke":67,"style":595},"50","220","stroke-width:3",[597,598,599,603,605,608,610,613,615,618,621,624,627,630,633,636,639,642,645],"g",{"fill":79},[86,600],{"cx":601,"cy":137,"r":602},"54","2.5",[86,604],{"cx":111,"cy":137,"r":602},[86,606],{"cx":607,"cy":137,"r":602},"74",[86,609],{"cx":120,"cy":137,"r":602},[86,611],{"cx":612,"cy":137,"r":602},"94",[86,614],{"cx":126,"cy":137,"r":602},[86,616],{"cx":617,"cy":137,"r":602},"114",[86,619],{"cx":620,"cy":137,"r":602},"124",[86,622],{"cx":623,"cy":137,"r":602},"134",[86,625],{"cx":626,"cy":137,"r":602},"144",[86,628],{"cx":629,"cy":137,"r":602},"154",[86,631],{"cx":632,"cy":137,"r":602},"164",[86,634],{"cx":635,"cy":137,"r":602},"174",[86,637],{"cx":638,"cy":137,"r":602},"184",[86,640],{"cx":641,"cy":137,"r":602},"194",[86,643],{"cx":644,"cy":137,"r":602},"204",[86,646],{"cx":647,"cy":137,"r":602},"214",[51,649,651],{"x":587,"y":650,"style":96,"fill":97,"textAnchor":57},"200","vertices per metre",[44,653],{"x":654,"y":582,"width":583,"height":584,"rx":65,"fill":66,"stroke":84,"style":75},"269",[51,656,657],{"x":53,"y":588,"style":589,"fill":84,"textAnchor":57},"duplicates",[70,659],{"d":660,"fill":661,"stroke":67,"style":595},"M300 170 L380 110 L460 170","none",[86,663],{"cx":53,"cy":664,"r":665,"fill":661,"stroke":84,"style":68},"110","9",[86,667],{"cx":53,"cy":664,"r":668,"fill":84},"4",[51,670,671],{"x":53,"y":650,"style":96,"fill":97,"textAnchor":57},"two vertices, one position",[44,673],{"x":674,"y":582,"width":583,"height":584,"rx":65,"fill":66,"stroke":91,"style":75},"514",[51,676,678],{"x":677,"y":588,"style":589,"fill":91,"textAnchor":57},"625","spike",[70,680],{"d":681,"fill":661,"stroke":67,"style":595},"M540 170 L610 150 L700 90 L618 154 L710 170",[86,683],{"cx":684,"cy":685,"r":686,"fill":91},"700","90","5",[51,688,689],{"x":677,"y":650,"style":96,"fill":97,"textAnchor":57},"out and back at 4°",[209,691,693],{"className":211,"code":692,"language":213,"meta":214,"style":214},"import math\n\ndef turn_angles(points):\n    for a, b, c in zip(points, points[1:], points[2:]):\n        v1 = (a.x() - b.x(), a.y() - b.y())\n        v2 = (c.x() - b.x(), c.y() - b.y())\n        n1, n2 = math.hypot(*v1), math.hypot(*v2)\n        if n1 == 0 or n2 == 0:\n            yield b, 0.0\n            continue\n        cos = max(-1.0, min(1.0, (v1[0] * v2[0] + v1[1] * v2[1]) \u002F (n1 * n2)))\n        yield b, math.degrees(math.acos(cos))\n\nreport = []\nfor f in parcels.getFeatures():\n    g = f.geometry()\n    count = g.constGet().nCoordinates()\n    per_m = count \u002F max(g.length(), 1.0)\n    points = list(g.vertices())\n    duplicates = sum(1 for p, q in zip(points, points[1:]) if p == q)\n    spikes = [p for p, ang in turn_angles(points) if 0 \u003C ang \u003C 5]\n    if count > 5000 or per_m > 2 or duplicates or spikes:\n        report.append((f[\"parcel_ref\"], count, round(per_m, 2), duplicates, len(spikes)))\n\nfor row in sorted(report, key=lambda r: -r[1])[:25]:\n    print(row)\n",[195,694,695,702,706,718,745,766,785,806,833,844,849,922,930,934,944,956,967,978,1000,1014,1056,1095,1129,1158,1163,1205],{"__ignoreMap":214},[218,696,697,699],{"class":220,"line":221},[218,698,232],{"class":224},[218,700,701],{"class":228}," math\n",[218,703,704],{"class":220,"line":238},[218,705,242],{"emptyLinePlaceholder":241},[218,707,708,711,715],{"class":220,"line":245},[218,709,710],{"class":224},"def",[218,712,714],{"class":713},"svObZ"," turn_angles",[218,716,717],{"class":228},"(points):\n",[218,719,720,723,726,728,731,734,736,739,742],{"class":220,"line":270},[218,721,722],{"class":224},"    for",[218,724,725],{"class":228}," a, b, c ",[218,727,329],{"class":224},[218,729,730],{"class":264}," zip",[218,732,733],{"class":228},"(points, points[",[218,735,320],{"class":264},[218,737,738],{"class":228},":], points[",[218,740,741],{"class":264},"2",[218,743,744],{"class":228},":]):\n",[218,746,747,750,752,755,758,761,763],{"class":220,"line":284},[218,748,749],{"class":228},"        v1 ",[218,751,251],{"class":224},[218,753,754],{"class":228}," (a.x() ",[218,756,757],{"class":224},"-",[218,759,760],{"class":228}," b.x(), a.y() ",[218,762,757],{"class":224},[218,764,765],{"class":228}," b.y())\n",[218,767,768,771,773,776,778,781,783],{"class":220,"line":295},[218,769,770],{"class":228},"        v2 ",[218,772,251],{"class":224},[218,774,775],{"class":228}," (c.x() ",[218,777,757],{"class":224},[218,779,780],{"class":228}," b.x(), c.y() ",[218,782,757],{"class":224},[218,784,765],{"class":228},[218,786,787,790,792,795,798,801,803],{"class":220,"line":300},[218,788,789],{"class":228},"        n1, n2 ",[218,791,251],{"class":224},[218,793,794],{"class":228}," math.hypot(",[218,796,797],{"class":224},"*",[218,799,800],{"class":228},"v1), math.hypot(",[218,802,797],{"class":224},[218,804,805],{"class":228},"v2)\n",[218,807,808,811,814,817,820,823,826,828,830],{"class":220,"line":341},[218,809,810],{"class":224},"        if",[218,812,813],{"class":228}," n1 ",[218,815,816],{"class":224},"==",[218,818,819],{"class":264}," 0",[218,821,822],{"class":224}," or",[218,824,825],{"class":228}," n2 ",[218,827,816],{"class":224},[218,829,819],{"class":264},[218,831,832],{"class":228},":\n",[218,834,835,838,841],{"class":220,"line":346},[218,836,837],{"class":224},"            yield",[218,839,840],{"class":228}," b, ",[218,842,843],{"class":264},"0.0\n",[218,845,846],{"class":220,"line":363},[218,847,848],{"class":224},"            continue\n",[218,850,851,854,856,859,861,863,866,868,871,873,875,878,880,883,885,888,890,892,895,898,900,902,904,906,908,911,914,917,919],{"class":220,"line":374},[218,852,853],{"class":228},"        cos ",[218,855,251],{"class":224},[218,857,858],{"class":264}," max",[218,860,306],{"class":228},[218,862,757],{"class":224},[218,864,865],{"class":264},"1.0",[218,867,312],{"class":228},[218,869,870],{"class":264},"min",[218,872,306],{"class":228},[218,874,865],{"class":264},[218,876,877],{"class":228},", (v1[",[218,879,46],{"class":264},[218,881,882],{"class":228},"] ",[218,884,797],{"class":224},[218,886,887],{"class":228}," v2[",[218,889,46],{"class":264},[218,891,882],{"class":228},[218,893,894],{"class":224},"+",[218,896,897],{"class":228}," v1[",[218,899,320],{"class":264},[218,901,882],{"class":228},[218,903,797],{"class":224},[218,905,887],{"class":228},[218,907,320],{"class":264},[218,909,910],{"class":228},"]) ",[218,912,913],{"class":224},"\u002F",[218,915,916],{"class":228}," (n1 ",[218,918,797],{"class":224},[218,920,921],{"class":228}," n2)))\n",[218,923,924,927],{"class":220,"line":386},[218,925,926],{"class":224},"        yield",[218,928,929],{"class":228}," b, math.degrees(math.acos(cos))\n",[218,931,932],{"class":220,"line":392},[218,933,242],{"emptyLinePlaceholder":241},[218,935,936,939,941],{"class":220,"line":403},[218,937,938],{"class":228},"report ",[218,940,251],{"class":224},[218,942,943],{"class":228}," []\n",[218,945,946,948,951,953],{"class":220,"line":449},[218,947,349],{"class":224},[218,949,950],{"class":228}," f ",[218,952,329],{"class":224},[218,954,955],{"class":228}," parcels.getFeatures():\n",[218,957,959,962,964],{"class":220,"line":958},16,[218,960,961],{"class":228},"    g ",[218,963,251],{"class":224},[218,965,966],{"class":228}," f.geometry()\n",[218,968,970,973,975],{"class":220,"line":969},17,[218,971,972],{"class":228},"    count ",[218,974,251],{"class":224},[218,976,977],{"class":228}," g.constGet().nCoordinates()\n",[218,979,981,984,986,989,991,993,996,998],{"class":220,"line":980},18,[218,982,983],{"class":228},"    per_m ",[218,985,251],{"class":224},[218,987,988],{"class":228}," count ",[218,990,913],{"class":224},[218,992,858],{"class":264},[218,994,995],{"class":228},"(g.length(), ",[218,997,865],{"class":264},[218,999,492],{"class":228},[218,1001,1003,1006,1008,1011],{"class":220,"line":1002},19,[218,1004,1005],{"class":228},"    points ",[218,1007,251],{"class":224},[218,1009,1010],{"class":264}," list",[218,1012,1013],{"class":228},"(g.vertices())\n",[218,1015,1017,1020,1022,1025,1027,1029,1031,1034,1036,1038,1040,1042,1045,1048,1051,1053],{"class":220,"line":1016},20,[218,1018,1019],{"class":228},"    duplicates ",[218,1021,251],{"class":224},[218,1023,1024],{"class":264}," sum",[218,1026,306],{"class":228},[218,1028,320],{"class":264},[218,1030,323],{"class":224},[218,1032,1033],{"class":228}," p, q ",[218,1035,329],{"class":224},[218,1037,730],{"class":264},[218,1039,733],{"class":228},[218,1041,320],{"class":264},[218,1043,1044],{"class":228},":]) ",[218,1046,1047],{"class":224},"if",[218,1049,1050],{"class":228}," p ",[218,1052,816],{"class":224},[218,1054,1055],{"class":228}," q)\n",[218,1057,1059,1062,1064,1067,1069,1072,1074,1077,1079,1081,1084,1087,1090,1093],{"class":220,"line":1058},21,[218,1060,1061],{"class":228},"    spikes ",[218,1063,251],{"class":224},[218,1065,1066],{"class":228}," [p ",[218,1068,349],{"class":224},[218,1070,1071],{"class":228}," p, ang ",[218,1073,329],{"class":224},[218,1075,1076],{"class":228}," turn_angles(points) ",[218,1078,1047],{"class":224},[218,1080,819],{"class":264},[218,1082,1083],{"class":224}," \u003C",[218,1085,1086],{"class":228}," ang ",[218,1088,1089],{"class":224},"\u003C",[218,1091,1092],{"class":264}," 5",[218,1094,267],{"class":228},[218,1096,1098,1100,1102,1105,1108,1110,1113,1115,1118,1120,1123,1126],{"class":220,"line":1097},22,[218,1099,377],{"class":224},[218,1101,988],{"class":228},[218,1103,1104],{"class":224},">",[218,1106,1107],{"class":264}," 5000",[218,1109,822],{"class":224},[218,1111,1112],{"class":228}," per_m ",[218,1114,1104],{"class":224},[218,1116,1117],{"class":264}," 2",[218,1119,822],{"class":224},[218,1121,1122],{"class":228}," duplicates ",[218,1124,1125],{"class":224},"or",[218,1127,1128],{"class":228}," spikes:\n",[218,1130,1132,1135,1138,1141,1144,1147,1149,1152,1155],{"class":220,"line":1131},23,[218,1133,1134],{"class":228},"        report.append((f[",[218,1136,1137],{"class":257},"\"parcel_ref\"",[218,1139,1140],{"class":228},"], count, ",[218,1142,1143],{"class":264},"round",[218,1145,1146],{"class":228},"(per_m, ",[218,1148,741],{"class":264},[218,1150,1151],{"class":228},"), duplicates, ",[218,1153,1154],{"class":264},"len",[218,1156,1157],{"class":228},"(spikes)))\n",[218,1159,1161],{"class":220,"line":1160},24,[218,1162,242],{"emptyLinePlaceholder":241},[218,1164,1166,1168,1171,1173,1176,1179,1183,1186,1189,1191,1194,1196,1199,1202],{"class":220,"line":1165},25,[218,1167,349],{"class":224},[218,1169,1170],{"class":228}," row ",[218,1172,329],{"class":224},[218,1174,1175],{"class":264}," sorted",[218,1177,1178],{"class":228},"(report, ",[218,1180,1182],{"class":1181},"s9osk","key",[218,1184,1185],{"class":224},"=lambda",[218,1187,1188],{"class":228}," r: ",[218,1190,757],{"class":224},[218,1192,1193],{"class":228},"r[",[218,1195,320],{"class":264},[218,1197,1198],{"class":228},"])[:",[218,1200,1201],{"class":264},"25",[218,1203,1204],{"class":228},"]:\n",[218,1206,1208,1210],{"class":220,"line":1207},26,[218,1209,406],{"class":264},[218,1211,1212],{"class":228},"(row)\n",[14,1214,1215,498,1217,1220],{},[178,1216,497],{},[195,1218,1219],{},"nCoordinates()"," counts every vertex including ring closures and is cheap because it does not build point objects. Vertex density — vertices per metre of perimeter — is a better signal than raw count, because a large rural parcel legitimately has more vertices than a small urban one. Consecutive identical points are true duplicates; a turn angle between two consecutive segments of only a few degrees means the boundary went out and came almost straight back, which is the signature of a spike. A zero angle is reported for zero-length segments, which are duplicates by another name, so the spike test excludes it. The thresholds belong to your data: a hand-digitised cadastre and a GPS track need different numbers.",[167,1222,1224],{"id":1223},"edit-individual-vertices","Edit individual vertices",[14,1226,1227,1229],{},[195,1228,528],{}," has methods to move, insert and delete vertices by flat index. Each changes the geometry object in place and returns a success flag; nothing touches the layer until you write the geometry back.",[209,1231,1233],{"className":211,"code":1232,"language":213,"meta":214,"style":214},"from qgis.core import QgsGeometry, QgsPointXY\n\ng = QgsGeometry(feature.geometry())\n\nok = g.moveVertex(412503.25, 287114.80, 7)\nprint(\"moved:\", ok)\n\nok = g.insertVertex(412510.00, 287120.00, 8)\nprint(\"inserted before vertex 8:\", ok)\n\nok = g.deleteVertex(3)\nprint(\"deleted vertex 3:\", ok)\n\nclosest_point, index, before, after, sq_dist = g.closestVertex(QgsPointXY(412505, 287116))\nprint(\"closest vertex index\", index, \"at\", closest_point, \"distance\", sq_dist ** 0.5)\n",[195,1234,1235,1246,1250,1260,1264,1289,1301,1305,1329,1340,1344,1358,1369,1373,1394],{"__ignoreMap":214},[218,1236,1237,1239,1241,1243],{"class":220,"line":221},[218,1238,225],{"class":224},[218,1240,229],{"class":228},[218,1242,232],{"class":224},[218,1244,1245],{"class":228}," QgsGeometry, QgsPointXY\n",[218,1247,1248],{"class":220,"line":238},[218,1249,242],{"emptyLinePlaceholder":241},[218,1251,1252,1255,1257],{"class":220,"line":245},[218,1253,1254],{"class":228},"g ",[218,1256,251],{"class":224},[218,1258,1259],{"class":228}," QgsGeometry(feature.geometry())\n",[218,1261,1262],{"class":220,"line":270},[218,1263,242],{"emptyLinePlaceholder":241},[218,1265,1266,1269,1271,1274,1277,1279,1282,1284,1287],{"class":220,"line":284},[218,1267,1268],{"class":228},"ok ",[218,1270,251],{"class":224},[218,1272,1273],{"class":228}," g.moveVertex(",[218,1275,1276],{"class":264},"412503.25",[218,1278,312],{"class":228},[218,1280,1281],{"class":264},"287114.80",[218,1283,312],{"class":228},[218,1285,1286],{"class":264},"7",[218,1288,492],{"class":228},[218,1290,1291,1293,1295,1298],{"class":220,"line":295},[218,1292,303],{"class":264},[218,1294,306],{"class":228},[218,1296,1297],{"class":257},"\"moved:\"",[218,1299,1300],{"class":228},", ok)\n",[218,1302,1303],{"class":220,"line":300},[218,1304,242],{"emptyLinePlaceholder":241},[218,1306,1307,1309,1311,1314,1317,1319,1322,1324,1327],{"class":220,"line":341},[218,1308,1268],{"class":228},[218,1310,251],{"class":224},[218,1312,1313],{"class":228}," g.insertVertex(",[218,1315,1316],{"class":264},"412510.00",[218,1318,312],{"class":228},[218,1320,1321],{"class":264},"287120.00",[218,1323,312],{"class":228},[218,1325,1326],{"class":264},"8",[218,1328,492],{"class":228},[218,1330,1331,1333,1335,1338],{"class":220,"line":346},[218,1332,303],{"class":264},[218,1334,306],{"class":228},[218,1336,1337],{"class":257},"\"inserted before vertex 8:\"",[218,1339,1300],{"class":228},[218,1341,1342],{"class":220,"line":363},[218,1343,242],{"emptyLinePlaceholder":241},[218,1345,1346,1348,1350,1353,1356],{"class":220,"line":374},[218,1347,1268],{"class":228},[218,1349,251],{"class":224},[218,1351,1352],{"class":228}," g.deleteVertex(",[218,1354,1355],{"class":264},"3",[218,1357,492],{"class":228},[218,1359,1360,1362,1364,1367],{"class":220,"line":386},[218,1361,303],{"class":264},[218,1363,306],{"class":228},[218,1365,1366],{"class":257},"\"deleted vertex 3:\"",[218,1368,1300],{"class":228},[218,1370,1371],{"class":220,"line":392},[218,1372,242],{"emptyLinePlaceholder":241},[218,1374,1375,1378,1380,1383,1386,1388,1391],{"class":220,"line":403},[218,1376,1377],{"class":228},"closest_point, index, before, after, sq_dist ",[218,1379,251],{"class":224},[218,1381,1382],{"class":228}," g.closestVertex(QgsPointXY(",[218,1384,1385],{"class":264},"412505",[218,1387,312],{"class":228},[218,1389,1390],{"class":264},"287116",[218,1392,1393],{"class":228},"))\n",[218,1395,1396,1398,1400,1403,1406,1409,1412,1415,1418,1421,1424],{"class":220,"line":449},[218,1397,303],{"class":264},[218,1399,306],{"class":228},[218,1401,1402],{"class":257},"\"closest vertex index\"",[218,1404,1405],{"class":228},", index, ",[218,1407,1408],{"class":257},"\"at\"",[218,1410,1411],{"class":228},", closest_point, ",[218,1413,1414],{"class":257},"\"distance\"",[218,1416,1417],{"class":228},", sq_dist ",[218,1419,1420],{"class":224},"**",[218,1422,1423],{"class":264}," 0.5",[218,1425,492],{"class":228},[14,1427,1428,1430,1431,1434,1435,1437,1438,1440,1441,1444,1445,1448,1449,1452,1453,1456,1457,1460,1461,1465],{},[178,1429,497],{}," Copying the geometry first with ",[195,1432,1433],{},"QgsGeometry(…)"," keeps the original feature's geometry untouched, so you can compare before and after. ",[195,1436,524],{}," has an overload taking a ",[195,1439,201],{},", which preserves Z and M. ",[195,1442,1443],{},"insertVertex"," inserts ",[204,1446,1447],{},"before"," the given index, shifting later indices up by one — so when inserting several vertices, work from the highest index down, or indices drift under you. ",[195,1450,1451],{},"deleteVertex"," refuses deletions that would leave an invalid ring, such as reducing a triangle to two vertices. ",[195,1454,1455],{},"closestVertex"," returns the nearest vertex, its index, its neighbours and the ",[204,1458,1459],{},"squared"," distance, which is the usual way to pick a vertex from a map click in a custom tool like the one in ",[21,1462,1464],{"href":1463},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis\u002F","snapping to features with QgsSnappingUtils",".",[167,1467,1469],{"id":1468},"clean-coordinates-in-bulk","Clean coordinates in bulk",[14,1471,1472],{},"Rather than touching vertices one at a time, several whole-geometry methods rewrite every vertex at once: removing duplicates, snapping to a grid, and dropping Z or M.",[14,1474,1475],{},[29,1476,1479,1482,1485,1488,1500,1503,1509,1513,1515,1518,1524,1527,1531,1534,1537,1541,1544,1548,1551,1553,1557,1561,1566,1569,1572,1579],{"viewBox":1477,"role":32,"ariaLabel":1478,"xmlns":34},"0 0 760 244","Bulk vertex clean-up steps applied in order: remove duplicate nodes, snap to a grid at survey precision, drop unwanted Z and M, then validate and fix, with the vertex count falling at each step",[36,1480,1481],{},"A clean-up pipeline for coordinates",[40,1483,1484],{},"Four steps left to right with a vertex count under each. Starting at 18,204 vertices, removeDuplicateNodes leaves 17,950. snappedToGrid at 1 centimetre leaves 17,610 because some vertices now coincide. dropping Z and M does not change the count but shrinks storage. A final validity check with makeValid catches any self-intersections the snapping introduced.",[44,1486],{"x":46,"y":46,"width":47,"height":1487,"fill":49},"244",[1489,1490,1491],"defs",{},[1492,1493,1497],"marker",{"id":1494,"viewBox":1495,"refX":1326,"refY":686,"markerWidth":1286,"markerHeight":1286,"orient":1496},"gvCleanArrow","0 0 10 10","auto-start-reverse",[70,1498],{"d":1499,"fill":97},"M0 0 L10 5 L0 10 z",[51,1501,1502],{"x":53,"y":139,"style":55,"fill":56,"textAnchor":57},"Clean in this order, then validate",[44,1504],{"x":1505,"y":1506,"width":1507,"height":1508,"rx":65,"fill":83,"stroke":84,"style":68},"16","60","160","96",[51,1510,1512],{"x":1508,"y":1511,"style":164,"fill":84,"textAnchor":57},"92","remove",[51,1514,657],{"x":1508,"y":664,"style":164,"fill":84,"textAnchor":57},[51,1516,1517],{"x":1508,"y":137,"style":132,"fill":97,"textAnchor":57},"17,950",[220,1519],{"x1":1520,"y1":1521,"x2":1522,"y2":1521,"stroke":97,"style":1523},"176","108","196","stroke-width:1.8;marker-end:url(#gvCleanArrow)",[44,1525],{"x":1526,"y":1506,"width":1507,"height":1508,"rx":65,"fill":114,"stroke":115,"style":68},"202",[51,1528,1530],{"x":1529,"y":1511,"style":164,"fill":115,"textAnchor":57},"282","snap to grid",[51,1532,1533],{"x":1529,"y":664,"style":96,"fill":97,"textAnchor":57},"1 cm precision",[51,1535,1536],{"x":1529,"y":137,"style":132,"fill":97,"textAnchor":57},"17,610",[220,1538],{"x1":1539,"y1":1521,"x2":1540,"y2":1521,"stroke":97,"style":1523},"362","382",[44,1542],{"x":1543,"y":1506,"width":1507,"height":1508,"rx":65,"fill":150,"stroke":79,"style":68},"388",[51,1545,1547],{"x":1546,"y":1511,"style":164,"fill":79,"textAnchor":57},"468","drop Z \u002F M",[51,1549,1550],{"x":1546,"y":664,"style":96,"fill":97,"textAnchor":57},"if not needed",[51,1552,1536],{"x":1546,"y":137,"style":132,"fill":97,"textAnchor":57},[220,1554],{"x1":1555,"y1":1521,"x2":1556,"y2":1521,"stroke":97,"style":1523},"548","568",[44,1558],{"x":1559,"y":1506,"width":1560,"height":1508,"rx":65,"fill":73,"stroke":74,"style":68},"574","170",[51,1562,1565],{"x":1563,"y":1511,"style":164,"fill":1564,"textAnchor":57},"659","#166534","validate",[51,1567,1568],{"x":1563,"y":664,"style":96,"fill":97,"textAnchor":57},"makeValid if needed",[51,1570,1571],{"x":1563,"y":137,"style":132,"fill":97,"textAnchor":57},"ok",[44,1573],{"x":1574,"y":1575,"width":1576,"height":1577,"rx":1326,"fill":66,"stroke":67,"style":1578},"120","186","520","38","stroke-width:1.8",[51,1580,1582],{"x":53,"y":102,"style":1581,"fill":97,"textAnchor":57},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","start: 18,204 vertices — snapping can create new duplicates and self-intersections",[209,1584,1586],{"className":211,"code":1585,"language":213,"meta":214,"style":214},"def clean(geom, grid=0.01, keep_z=False):\n    g = QgsGeometry(geom)\n    g.removeDuplicateNodes(1e-8)\n    g = g.snappedToGrid(grid, grid)\n    g.removeDuplicateNodes(1e-8)\n    abstract = g.get()\n    if not keep_z:\n        abstract.dropZValue()\n        abstract.dropMValue()\n    if not g.isGeosValid():\n        g = g.makeValid()\n    return g\n\nbefore = feature.geometry()\nafter = clean(before)\nprint(before.constGet().nCoordinates(), \"→\", after.constGet().nCoordinates(),\n      \"area change:\", round(after.area() - before.area(), 4), \"m²\")\n",[195,1587,1588,1614,1623,1633,1642,1650,1660,1669,1674,1679,1688,1698,1706,1710,1719,1729,1742],{"__ignoreMap":214},[218,1589,1590,1592,1595,1598,1600,1603,1606,1608,1611],{"class":220,"line":221},[218,1591,710],{"class":224},[218,1593,1594],{"class":713}," clean",[218,1596,1597],{"class":228},"(geom, grid",[218,1599,251],{"class":224},[218,1601,1602],{"class":264},"0.01",[218,1604,1605],{"class":228},", keep_z",[218,1607,251],{"class":224},[218,1609,1610],{"class":264},"False",[218,1612,1613],{"class":228},"):\n",[218,1615,1616,1618,1620],{"class":220,"line":238},[218,1617,961],{"class":228},[218,1619,251],{"class":224},[218,1621,1622],{"class":228}," QgsGeometry(geom)\n",[218,1624,1625,1628,1631],{"class":220,"line":245},[218,1626,1627],{"class":228},"    g.removeDuplicateNodes(",[218,1629,1630],{"class":264},"1e-8",[218,1632,492],{"class":228},[218,1634,1635,1637,1639],{"class":220,"line":270},[218,1636,961],{"class":228},[218,1638,251],{"class":224},[218,1640,1641],{"class":228}," g.snappedToGrid(grid, grid)\n",[218,1643,1644,1646,1648],{"class":220,"line":284},[218,1645,1627],{"class":228},[218,1647,1630],{"class":264},[218,1649,492],{"class":228},[218,1651,1652,1655,1657],{"class":220,"line":295},[218,1653,1654],{"class":228},"    abstract ",[218,1656,251],{"class":224},[218,1658,1659],{"class":228}," g.get()\n",[218,1661,1662,1664,1666],{"class":220,"line":300},[218,1663,377],{"class":224},[218,1665,380],{"class":224},[218,1667,1668],{"class":228}," keep_z:\n",[218,1670,1671],{"class":220,"line":341},[218,1672,1673],{"class":228},"        abstract.dropZValue()\n",[218,1675,1676],{"class":220,"line":346},[218,1677,1678],{"class":228},"        abstract.dropMValue()\n",[218,1680,1681,1683,1685],{"class":220,"line":363},[218,1682,377],{"class":224},[218,1684,380],{"class":224},[218,1686,1687],{"class":228}," g.isGeosValid():\n",[218,1689,1690,1693,1695],{"class":220,"line":374},[218,1691,1692],{"class":228},"        g ",[218,1694,251],{"class":224},[218,1696,1697],{"class":228}," g.makeValid()\n",[218,1699,1700,1703],{"class":220,"line":386},[218,1701,1702],{"class":224},"    return",[218,1704,1705],{"class":228}," g\n",[218,1707,1708],{"class":220,"line":392},[218,1709,242],{"emptyLinePlaceholder":241},[218,1711,1712,1715,1717],{"class":220,"line":403},[218,1713,1714],{"class":228},"before ",[218,1716,251],{"class":224},[218,1718,292],{"class":228},[218,1720,1721,1724,1726],{"class":220,"line":449},[218,1722,1723],{"class":228},"after ",[218,1725,251],{"class":224},[218,1727,1728],{"class":228}," clean(before)\n",[218,1730,1731,1733,1736,1739],{"class":220,"line":958},[218,1732,303],{"class":264},[218,1734,1735],{"class":228},"(before.constGet().nCoordinates(), ",[218,1737,1738],{"class":257},"\"→\"",[218,1740,1741],{"class":228},", after.constGet().nCoordinates(),\n",[218,1743,1744,1747,1749,1751,1754,1756,1759,1761,1764,1767],{"class":220,"line":969},[218,1745,1746],{"class":257},"      \"area change:\"",[218,1748,312],{"class":228},[218,1750,1143],{"class":264},[218,1752,1753],{"class":228},"(after.area() ",[218,1755,757],{"class":224},[218,1757,1758],{"class":228}," before.area(), ",[218,1760,668],{"class":264},[218,1762,1763],{"class":228},"), ",[218,1765,1766],{"class":257},"\"m²\"",[218,1768,492],{"class":228},[14,1770,1771,1773,1774,1777,1778,1780,1781,1784,1785,1789],{},[178,1772,497],{}," Snapping to a one-centimetre grid rounds every coordinate to the precision the survey actually has, which removes meaningless digits and makes near-identical vertices exactly identical — hence the second duplicate removal. ",[195,1775,1776],{},"get()"," returns a mutable pointer to the abstract geometry, unlike ",[195,1779,501],{},", and is what allows dropping Z and M in place. Snapping can create self-intersections where two vertices of a thin sliver collapse together, so validity is checked last and repaired with ",[195,1782,1783],{},"makeValid",", as covered in ",[21,1786,1788],{"href":1787},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Ffix-invalid-geometries-pyqgis\u002F","finding and fixing invalid geometries",". Reporting the area change is a sanity check: at centimetre precision it should be a fraction of a square metre.",[167,1791,1793],{"id":1792},"write-edited-geometries-back","Write edited geometries back",[14,1795,1796],{},"Edited geometries live only in Python until they are written to the layer. An edit session groups the changes so they commit together or roll back together.",[209,1798,1800],{"className":211,"code":1799,"language":213,"meta":214,"style":214},"from qgis.core import edit\n\nchanged = 0\nwith edit(parcels):\n    for f in parcels.getFeatures():\n        new_geom = clean(f.geometry())\n        if not new_geom.equals(f.geometry()):\n            parcels.changeGeometry(f.id(), new_geom)\n            changed += 1\nprint(changed, \"geometries changed\")\n",[195,1801,1802,1813,1817,1827,1835,1845,1855,1864,1869,1880],{"__ignoreMap":214},[218,1803,1804,1806,1808,1810],{"class":220,"line":221},[218,1805,225],{"class":224},[218,1807,229],{"class":228},[218,1809,232],{"class":224},[218,1811,1812],{"class":228}," edit\n",[218,1814,1815],{"class":220,"line":238},[218,1816,242],{"emptyLinePlaceholder":241},[218,1818,1819,1822,1824],{"class":220,"line":245},[218,1820,1821],{"class":228},"changed ",[218,1823,251],{"class":224},[218,1825,1826],{"class":264}," 0\n",[218,1828,1829,1832],{"class":220,"line":270},[218,1830,1831],{"class":224},"with",[218,1833,1834],{"class":228}," edit(parcels):\n",[218,1836,1837,1839,1841,1843],{"class":220,"line":284},[218,1838,722],{"class":224},[218,1840,950],{"class":228},[218,1842,329],{"class":224},[218,1844,955],{"class":228},[218,1846,1847,1850,1852],{"class":220,"line":295},[218,1848,1849],{"class":228},"        new_geom ",[218,1851,251],{"class":224},[218,1853,1854],{"class":228}," clean(f.geometry())\n",[218,1856,1857,1859,1861],{"class":220,"line":300},[218,1858,810],{"class":224},[218,1860,380],{"class":224},[218,1862,1863],{"class":228}," new_geom.equals(f.geometry()):\n",[218,1865,1866],{"class":220,"line":341},[218,1867,1868],{"class":228},"            parcels.changeGeometry(f.id(), new_geom)\n",[218,1870,1871,1874,1877],{"class":220,"line":346},[218,1872,1873],{"class":228},"            changed ",[218,1875,1876],{"class":224},"+=",[218,1878,1879],{"class":264}," 1\n",[218,1881,1882,1884,1887,1890],{"class":220,"line":363},[218,1883,303],{"class":264},[218,1885,1886],{"class":228},"(changed, ",[218,1888,1889],{"class":257},"\"geometries changed\"",[218,1891,492],{"class":228},[14,1893,1894,1896,1897,1900,1901,1904,1905,1908,1909,1912,1913,1917,1918,1921],{},[178,1895,497],{}," Comparing with ",[195,1898,1899],{},"equals"," skips features the clean-up did not alter, which keeps the edit buffer small and the undo history meaningful. ",[195,1902,1903],{},"changeGeometry"," checks that the new geometry type is compatible with the layer — dropping Z from a ",[195,1906,1907],{},"PolygonZ"," layer is refused, so change the layer's geometry type by writing a new layer instead in that case. The ",[195,1910,1911],{},"edit"," context manager commits on success and rolls back if anything raises, as in ",[21,1914,1916],{"href":1915},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002F","editing features with transactions",". For a read-only exploration of vertices as their own features, ",[195,1919,1920],{},"native:extractvertices"," produces a point layer with part, ring and vertex index fields — useful for styling or labelling vertices on the map.",[167,1923,1925],{"id":1924},"qgis-version-compatibility","QGIS version compatibility",[14,1927,1928,312,1931,312,1934,312,1936,312,1938,510,1940,1942,1943,1946,1947,1950,1951,1953],{},[195,1929,1930],{},"vertices()",[195,1932,1933],{},"nextVertex",[195,1935,524],{},[195,1937,1443],{},[195,1939,1451],{},[195,1941,1455],{}," have been available since QGIS 3.0. ",[195,1944,1945],{},"removeDuplicateNodes"," arrived in 3.4 and ",[195,1948,1949],{},"snappedToGrid"," in 3.2. ",[195,1952,1783],{}," uses GEOS's implementation from 3.16, and gained a method argument in 3.28. All of them work identically on 3.40, 3.44 and the QGIS 4 series.",[167,1955,1957],{"id":1956},"troubleshooting","Troubleshooting",[172,1959,1960,1966,1976,1984,1997],{},[175,1961,1962,1965],{},[178,1963,1964],{},"Indices shift after inserting."," Insert from the highest index down.",[175,1967,1968,1973,1974,1465],{},[178,1969,1970,1972],{},[195,1971,524],{}," returns False."," The index is beyond the vertex count; check against ",[195,1975,1219],{},[175,1977,1978,1983],{},[178,1979,1980,1982],{},[195,1981,1903],{}," fails."," The geometry type no longer matches the layer, for example after dropping Z.",[175,1985,1986,498,1989,1992,1993,1996],{},[178,1987,1988],{},"Z values disappear.",[195,1990,1991],{},"asPolyline"," or ",[195,1994,1995],{},"asPolygon"," was used; they return 2D points.",[175,1998,1999,2002],{},[178,2000,2001],{},"Area changes noticeably after snapping."," The grid is too coarse for the data's scale.",[167,2004,2006],{"id":2005},"conclusion","Conclusion",[14,2008,2009,2010,2012,2013,2015],{},"Iterate with ",[195,2011,1930],{}," for simple passes and ",[195,2014,1933],{}," when you need parts and rings, convert between structured and flat indices as needed, and use vertex density, duplicates and turn angles to find geometries that are valid but wrong. Edit on a copy, clean in bulk with duplicate removal, grid snapping and a final validity check, and write back only the geometries that changed inside one edit session.",[167,2017,2019],{"id":2018},"frequently-asked-questions","Frequently Asked Questions",[14,2021,2022,2025,2026,2029,2030,2033],{},[178,2023,2024],{},"How do I count vertices for every feature quickly?","\nUse ",[195,2027,2028],{},"feature.geometry().constGet().nCoordinates()",", or the expression ",[195,2031,2032],{},"num_points($geometry)"," in a field calculation.",[14,2035,2036,2039,2040,2042,2043,1992,2046,2049],{},[178,2037,2038],{},"Can I iterate vertices of curved geometries?","\nYes. ",[195,2041,1930],{}," returns the control points of curves; segmentize with ",[195,2044,2045],{},"densifyByCount",[195,2047,2048],{},"segmentize"," if you need the approximated line.",[14,2051,2052,2060,2062,2063,2065],{},[178,2053,2054,2055,510,2057,2059],{},"What is the difference between ",[195,2056,1776],{},[195,2058,501],{},"?",[195,2061,501],{}," returns a read-only view without copying; ",[195,2064,1776],{}," detaches a copy that you may modify in place.",[14,2067,2068,2071],{},[178,2069,2070],{},"Does snapping to a grid preserve topology between neighbours?","\nMostly, because shared vertices round to the same grid point. Check with a topology validation afterwards if boundaries must stay coincident.",[167,2073,2075],{"id":2074},"related","Related",[172,2077,2078,2083,2088,2094,2100],{},[175,2079,2080,2082],{},[21,2081,24],{"href":23}," — the guide this recipe belongs to",[175,2084,2085],{},[21,2086,2087],{"href":1787},"Find and Fix Invalid Geometries in PyQGIS",[175,2089,2090],{},[21,2091,2093],{"href":2092},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fsimplify-geometry-pyqgis\u002F","Simplify Geometry in PyQGIS",[175,2095,2096],{},[21,2097,2099],{"href":2098},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis\u002F","Linear Referencing Along Lines in PyQGIS",[175,2101,2102],{},[21,2103,2105],{"href":2104},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fconvert-multipart-to-singlepart-pyqgis\u002F","Convert Multipart to Singlepart Geometries in PyQGIS",[2107,2108,2109],"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 .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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":214,"searchDepth":238,"depth":238,"links":2111},[2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122],{"id":169,"depth":238,"text":170},{"id":190,"depth":238,"text":191},{"id":557,"depth":238,"text":558},{"id":1223,"depth":238,"text":1224},{"id":1468,"depth":238,"text":1469},{"id":1792,"depth":238,"text":1793},{"id":1924,"depth":238,"text":1925},{"id":1956,"depth":238,"text":1957},{"id":2005,"depth":238,"text":2006},{"id":2018,"depth":238,"text":2019},{"id":2074,"depth":238,"text":2075},"Walk every vertex of points, lines and polygons — parts, rings and vertex ids included — then move, insert and delete vertices, remove duplicates, snap coordinates to a grid and write the edited geometry back safely.","md",{"slug":2126,"type":2127,"breadcrumb":2128,"datePublished":2129,"dateModified":2129},"iterate-and-edit-geometry-vertices-pyqgis","article","Iterate and Edit Geometry Vertices","2026-09-17","\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fiterate-and-edit-geometry-vertices-pyqgis",{"title":5,"description":2123},"spatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fiterate-and-edit-geometry-vertices-pyqgis\u002Findex","eRb23ILs2eWDRQDCZlfb-ysbsUH-hRtNkKMHE6T53IQ",1789632908411]