[{"data":1,"prerenderedAt":2124},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fgenerate-points-along-lines-pyqgis":3},{"id":4,"title":5,"body":6,"description":2113,"extension":2114,"meta":2115,"navigation":201,"path":2120,"seo":2121,"stem":2122,"__hash__":2123},"docs\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fgenerate-points-along-lines-pyqgis\u002Findex.md","Generate Points Along Lines in PyQGIS",{"type":7,"value":8,"toc":2098},"minimark",[9,13,22,31,132,137,150,154,160,357,376,380,383,460,625,638,642,645,793,810,814,820,1127,1149,1153,1160,1452,1457,1461,1464,1509,1801,1820,1824,1827,1927,1949,1953,1972,1976,2002,2006,2015,2019,2029,2045,2051,2061,2065,2094],[10,11,5],"h1",{"id":12},"generate-points-along-lines-in-pyqgis",[14,15,16,17,21],"p",{},"Points spaced along lines are a building block for a surprising number of tasks: kilometre markers along a road, inspection stations every 50 m along a pipe, sampling points for an elevation or noise profile along a river, label anchors along a trail, or evenly spaced observation points for a viewshed. QGIS can generate them in one Processing call or with full control from ",[18,19,20],"code",{},"QgsGeometry.interpolate",", and either way each point can carry its distance along the line and the line's direction at that point.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002F","Vector Data Manipulation",". It generates points at fixed intervals with Processing, adds chainage and direction, controls start and end offsets, writes a custom generator for special spacing rules, and samples raster values at the generated points.",[14,32,33],{},[34,35,40,44,48,55,64,71,79,83,87,91,95,99,103,107,113,117,121,127],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 270","img","Points placed every 100 metres along a curved line, each with chainage and direction, with optional start and end offsets","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Points at a fixed spacing",[45,46,47],"desc",{},"A curved line with points placed every 100 metres from its start. Each point carries its distance along the line, its chainage, and the line's direction at that point as an angle. A start offset shifts the first point away from the start; an end offset stops points short of the end. The last segment is usually shorter than the spacing.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","270","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Every 100 m, with distance and direction",[65,66],"path",{"d":67,"fill":68,"stroke":69,"style":70},"M 60 200 C 180 60 320 60 420 140 C 500 200 600 200 700 90","none","#2563eb","stroke-width:3",[72,73],"circle",{"cx":74,"cy":75,"r":76,"fill":77,"stroke":61,"style":78},"60","200","6","#b45309","stroke-width:1",[72,80],{"cx":81,"cy":82,"r":76,"fill":77,"stroke":61,"style":78},"130","128",[72,84],{"cx":85,"cy":86,"r":76,"fill":77,"stroke":61,"style":78},"215","88",[72,88],{"cx":89,"cy":90,"r":76,"fill":77,"stroke":61,"style":78},"305","90",[72,92],{"cx":93,"cy":94,"r":76,"fill":77,"stroke":61,"style":78},"390","120",[72,96],{"cx":97,"cy":98,"r":76,"fill":77,"stroke":61,"style":78},"470","172",[72,100],{"cx":101,"cy":102,"r":76,"fill":77,"stroke":61,"style":78},"560","186",[72,104],{"cx":105,"cy":106,"r":76,"fill":77,"stroke":61,"style":78},"645","150",[56,108,112],{"x":74,"y":109,"style":110,"fill":111,"textAnchor":62},"224","text-anchor:middle;font-size:10.0px;font-family:monospace","#2f3b35","0 m",[56,114,116],{"x":85,"y":115,"style":110,"fill":111,"textAnchor":62},"70","200 m",[56,118,120],{"x":97,"y":119,"style":110,"fill":111,"textAnchor":62},"196","500 m",[56,122,126],{"x":123,"y":115,"style":124,"fill":125,"textAnchor":62},"700","text-anchor:middle;font-size:10.0px;font-family:sans-serif","#59645f","end",[56,128,131],{"x":58,"y":129,"style":130,"fill":125,"textAnchor":62},"252","text-anchor:middle;font-size:10.5px;font-family:sans-serif","each point: distance along line · angle of the line",[133,134,136],"h2",{"id":135},"prerequisites","Prerequisites",[138,139,140,144,147],"ul",{},[141,142,143],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[141,145,146],{},"A line layer in a projected CRS with metre units, so spacing is in metres.",[141,148,149],{},"Clean, connected lines. A road split into many short segments gives points restarting at zero on every segment; merge segments into routes first if chainage should run continuously.",[133,151,153],{"id":152},"generate-points-with-processing","Generate points with Processing",[14,155,156,159],{},[18,157,158],{},"native:pointsalonglines"," places points at a fixed distance along every line, with optional offsets at both ends, and records distance and angle.",[161,162,167],"pre",{"className":163,"code":164,"language":165,"meta":166,"style":166},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject\n\npipes = QgsProject.instance().mapLayersByName(\"water_mains\")[0]\nstations = processing.run(\"native:pointsalonglines\", {\n    \"INPUT\": pipes,\n    \"DISTANCE\": 50,          # metres between points\n    \"START_OFFSET\": 0,\n    \"END_OFFSET\": 0,\n    \"OUTPUT\": \"memory:inspection_stations\",\n})[\"OUTPUT\"]\n\nprint(stations.featureCount(), \"stations;\", stations.fields().names()[-2:])\nQgsProject.instance().addMapLayer(stations)\n","python","",[18,168,169,182,196,203,228,245,254,273,286,298,311,322,327,351],{"__ignoreMap":166},[170,171,174,178],"span",{"class":172,"line":173},"line",1,[170,175,177],{"class":176},"snl16","import",[170,179,181],{"class":180},"s95oV"," processing\n",[170,183,185,188,191,193],{"class":172,"line":184},2,[170,186,187],{"class":176},"from",[170,189,190],{"class":180}," qgis.core ",[170,192,177],{"class":176},[170,194,195],{"class":180}," QgsProject\n",[170,197,199],{"class":172,"line":198},3,[170,200,202],{"emptyLinePlaceholder":201},true,"\n",[170,204,206,209,212,215,219,222,225],{"class":172,"line":205},4,[170,207,208],{"class":180},"pipes ",[170,210,211],{"class":176},"=",[170,213,214],{"class":180}," QgsProject.instance().mapLayersByName(",[170,216,218],{"class":217},"sU2Wk","\"water_mains\"",[170,220,221],{"class":180},")[",[170,223,51],{"class":224},"sDLfK",[170,226,227],{"class":180},"]\n",[170,229,231,234,236,239,242],{"class":172,"line":230},5,[170,232,233],{"class":180},"stations ",[170,235,211],{"class":176},[170,237,238],{"class":180}," processing.run(",[170,240,241],{"class":217},"\"native:pointsalonglines\"",[170,243,244],{"class":180},", {\n",[170,246,248,251],{"class":172,"line":247},6,[170,249,250],{"class":217},"    \"INPUT\"",[170,252,253],{"class":180},": pipes,\n",[170,255,257,260,263,266,269],{"class":172,"line":256},7,[170,258,259],{"class":217},"    \"DISTANCE\"",[170,261,262],{"class":180},": ",[170,264,265],{"class":224},"50",[170,267,268],{"class":180},",          ",[170,270,272],{"class":271},"sjoCn","# metres between points\n",[170,274,276,279,281,283],{"class":172,"line":275},8,[170,277,278],{"class":217},"    \"START_OFFSET\"",[170,280,262],{"class":180},[170,282,51],{"class":224},[170,284,285],{"class":180},",\n",[170,287,289,292,294,296],{"class":172,"line":288},9,[170,290,291],{"class":217},"    \"END_OFFSET\"",[170,293,262],{"class":180},[170,295,51],{"class":224},[170,297,285],{"class":180},[170,299,301,304,306,309],{"class":172,"line":300},10,[170,302,303],{"class":217},"    \"OUTPUT\"",[170,305,262],{"class":180},[170,307,308],{"class":217},"\"memory:inspection_stations\"",[170,310,285],{"class":180},[170,312,314,317,320],{"class":172,"line":313},11,[170,315,316],{"class":180},"})[",[170,318,319],{"class":217},"\"OUTPUT\"",[170,321,227],{"class":180},[170,323,325],{"class":172,"line":324},12,[170,326,202],{"emptyLinePlaceholder":201},[170,328,330,333,336,339,342,345,348],{"class":172,"line":329},13,[170,331,332],{"class":224},"print",[170,334,335],{"class":180},"(stations.featureCount(), ",[170,337,338],{"class":217},"\"stations;\"",[170,340,341],{"class":180},", stations.fields().names()[",[170,343,344],{"class":176},"-",[170,346,347],{"class":224},"2",[170,349,350],{"class":180},":])\n",[170,352,354],{"class":172,"line":353},14,[170,355,356],{"class":180},"QgsProject.instance().addMapLayer(stations)\n",[14,358,359,363,364,367,368,371,372,375],{},[360,361,362],"strong",{},"Breakdown:"," Each output point keeps the attributes of its source line and gains a ",[18,365,366],{},"distance"," field (metres from the start of that line) and an ",[18,369,370],{},"angle"," field (the line's direction at the point in degrees clockwise from north). Start and end offsets keep points away from line ends, which is useful when ends are junctions where a station would be ambiguous. The spacing can be data-defined — ",[18,373,374],{},"QgsProperty.fromExpression('CASE WHEN \"diameter_mm\" > 300 THEN 25 ELSE 50 END')"," gives larger mains denser stations.",[133,377,379],{"id":378},"make-chainage-continuous-along-routes","Make chainage continuous along routes",[14,381,382],{},"Network layers are often split at every junction, so a road might be thirty separate features. Points generated on each restart at zero, which makes \"kilometre 3.2 of route B7\" impossible to read off.",[14,384,385],{},[34,386,389,392,395,398,413,416,424,430,434,438,443,448,451,454],{"viewBox":387,"role":37,"ariaLabel":388,"xmlns":39},"0 0 760 222","Points on separate segments restarting at zero, compared with points on a merged route carrying continuous chainage",[41,390,391],{},"Segments versus routes",[45,393,394],{},"A road stored as three segments produces points whose distance restarts at zero on each segment. Dissolving the segments by route and merging them into one continuous line first gives one line per route, and points generated on it carry continuous chainage from the route's start.",[49,396],{"x":51,"y":51,"width":52,"height":397,"fill":54},"222",[399,400,401],"defs",{},[402,403,410],"marker",{"id":404,"viewBox":405,"refX":406,"refY":407,"markerWidth":408,"markerHeight":408,"orient":409},"palChainArrow","0 0 10 10","8","5","7","auto-start-reverse",[65,411],{"d":412,"fill":111},"M0 0 L10 5 L0 10 z",[56,414,415],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Merge segments into routes first",[49,417],{"x":418,"y":419,"width":420,"height":106,"rx":406,"fill":421,"stroke":422,"style":423},"24","56","340","#fdf2e2","#b91c1c","stroke-width:2",[56,425,429],{"x":426,"y":427,"style":428,"fill":422,"textAnchor":62},"194","104.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","per segment",[56,431,433],{"x":426,"y":432,"style":124,"fill":111,"textAnchor":62},"134.6","0 · 50 · 100 | 0 · 50 | 0 · 50",[56,435,437],{"x":426,"y":436,"style":124,"fill":125,"textAnchor":62},"164.6","chainage restarts",[49,439],{"x":440,"y":419,"width":420,"height":106,"rx":406,"fill":441,"stroke":442,"style":423},"396","#e8efe6","#15803d",[56,444,447],{"x":445,"y":427,"style":428,"fill":446,"textAnchor":62},"566","#166534","per route",[56,449,450],{"x":445,"y":432,"style":124,"fill":111,"textAnchor":62},"0 · 50 · 100 · 150 · 200 · 250",[56,452,453],{"x":445,"y":436,"style":124,"fill":125,"textAnchor":62},"continuous chainage",[172,455],{"x1":456,"y1":457,"x2":458,"y2":457,"stroke":111,"style":459},"364","131","392","stroke-width:1.8;marker-end:url(#palChainArrow)",[161,461,463],{"className":163,"code":462,"language":165,"meta":166,"style":166},"routes = processing.run(\"native:dissolve\", {\n    \"INPUT\": QgsProject.instance().mapLayersByName(\"roads\")[0],\n    \"FIELD\": [\"route_ref\"], \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\nmerged = processing.run(\"native:mergelines\", {\n    \"INPUT\": routes, \"OUTPUT\": \"memory:routes_merged\"})[\"OUTPUT\"]\n\nkm_posts = processing.run(\"native:pointsalonglines\", {\n    \"INPUT\": merged, \"DISTANCE\": 1000, \"START_OFFSET\": 0, \"END_OFFSET\": 0,\n    \"OUTPUT\": \"memory:km_posts\"})[\"OUTPUT\"]\n",[18,464,465,479,496,523,537,557,561,574,610],{"__ignoreMap":166},[170,466,467,470,472,474,477],{"class":172,"line":173},[170,468,469],{"class":180},"routes ",[170,471,211],{"class":176},[170,473,238],{"class":180},[170,475,476],{"class":217},"\"native:dissolve\"",[170,478,244],{"class":180},[170,480,481,483,486,489,491,493],{"class":172,"line":184},[170,482,250],{"class":217},[170,484,485],{"class":180},": QgsProject.instance().mapLayersByName(",[170,487,488],{"class":217},"\"roads\"",[170,490,221],{"class":180},[170,492,51],{"class":224},[170,494,495],{"class":180},"],\n",[170,497,498,501,504,507,510,512,514,517,519,521],{"class":172,"line":198},[170,499,500],{"class":217},"    \"FIELD\"",[170,502,503],{"class":180},": [",[170,505,506],{"class":217},"\"route_ref\"",[170,508,509],{"class":180},"], ",[170,511,319],{"class":217},[170,513,262],{"class":180},[170,515,516],{"class":217},"\"memory:\"",[170,518,316],{"class":180},[170,520,319],{"class":217},[170,522,227],{"class":180},[170,524,525,528,530,532,535],{"class":172,"line":205},[170,526,527],{"class":180},"merged ",[170,529,211],{"class":176},[170,531,238],{"class":180},[170,533,534],{"class":217},"\"native:mergelines\"",[170,536,244],{"class":180},[170,538,539,541,544,546,548,551,553,555],{"class":172,"line":230},[170,540,250],{"class":217},[170,542,543],{"class":180},": routes, ",[170,545,319],{"class":217},[170,547,262],{"class":180},[170,549,550],{"class":217},"\"memory:routes_merged\"",[170,552,316],{"class":180},[170,554,319],{"class":217},[170,556,227],{"class":180},[170,558,559],{"class":172,"line":247},[170,560,202],{"emptyLinePlaceholder":201},[170,562,563,566,568,570,572],{"class":172,"line":256},[170,564,565],{"class":180},"km_posts ",[170,567,211],{"class":176},[170,569,238],{"class":180},[170,571,241],{"class":217},[170,573,244],{"class":180},[170,575,576,578,581,584,586,589,592,595,597,599,601,604,606,608],{"class":172,"line":275},[170,577,250],{"class":217},[170,579,580],{"class":180},": merged, ",[170,582,583],{"class":217},"\"DISTANCE\"",[170,585,262],{"class":180},[170,587,588],{"class":224},"1000",[170,590,591],{"class":180},", ",[170,593,594],{"class":217},"\"START_OFFSET\"",[170,596,262],{"class":180},[170,598,51],{"class":224},[170,600,591],{"class":180},[170,602,603],{"class":217},"\"END_OFFSET\"",[170,605,262],{"class":180},[170,607,51],{"class":224},[170,609,285],{"class":180},[170,611,612,614,616,619,621,623],{"class":172,"line":288},[170,613,303],{"class":217},[170,615,262],{"class":180},[170,617,618],{"class":217},"\"memory:km_posts\"",[170,620,316],{"class":180},[170,622,319],{"class":217},[170,624,227],{"class":180},[14,626,627,629,630,633,634,637],{},[360,628,362],{}," Dissolving by route reference gathers all segments of a route into one multiline; ",[18,631,632],{},"native:mergelines"," joins the parts that touch end to end into single continuous lines. Points on the merged lines carry chainage from the route's start. Routes with gaps or branches stay multipart and their parts are processed separately, so check the result where networks fork. Direction matters too: chainage runs from the line's first vertex, so reverse lines that were digitized against the route's official direction with ",[18,635,636],{},"native:reverselinedirection",".",[133,639,641],{"id":640},"add-readable-labels-and-attributes","Add readable labels and attributes",[14,643,644],{},"Raw distances in metres with decimals are not how people read markers. A computed field turns them into kilometre labels, and the angle can rotate symbols to sit across or along the line.",[161,646,648],{"className":163,"code":647,"language":165,"meta":166,"style":166},"labelled = processing.run(\"native:fieldcalculator\", {\n    \"INPUT\": km_posts, \"FIELD_NAME\": \"label\", \"FIELD_TYPE\": 2, \"FIELD_LENGTH\": 20,\n    \"FORMULA\": \"\\\"route_ref\\\" || ' km ' || format_number(\\\"distance\\\" \u002F 1000, 1)\",\n    \"OUTPUT\": \"memory:km_posts_labelled\"})[\"OUTPUT\"]\nfor f in list(labelled.getFeatures())[:3]:\n    print(f[\"label\"], round(f[\"angle\"]))\n",[18,649,650,664,700,732,747,770],{"__ignoreMap":166},[170,651,652,655,657,659,662],{"class":172,"line":173},[170,653,654],{"class":180},"labelled ",[170,656,211],{"class":176},[170,658,238],{"class":180},[170,660,661],{"class":217},"\"native:fieldcalculator\"",[170,663,244],{"class":180},[170,665,666,668,671,674,676,679,681,684,686,688,690,693,695,698],{"class":172,"line":184},[170,667,250],{"class":217},[170,669,670],{"class":180},": km_posts, ",[170,672,673],{"class":217},"\"FIELD_NAME\"",[170,675,262],{"class":180},[170,677,678],{"class":217},"\"label\"",[170,680,591],{"class":180},[170,682,683],{"class":217},"\"FIELD_TYPE\"",[170,685,262],{"class":180},[170,687,347],{"class":224},[170,689,591],{"class":180},[170,691,692],{"class":217},"\"FIELD_LENGTH\"",[170,694,262],{"class":180},[170,696,697],{"class":224},"20",[170,699,285],{"class":180},[170,701,702,705,707,710,713,716,718,721,723,725,727,730],{"class":172,"line":198},[170,703,704],{"class":217},"    \"FORMULA\"",[170,706,262],{"class":180},[170,708,709],{"class":217},"\"",[170,711,712],{"class":224},"\\\"",[170,714,715],{"class":217},"route_ref",[170,717,712],{"class":224},[170,719,720],{"class":217}," || ' km ' || format_number(",[170,722,712],{"class":224},[170,724,366],{"class":217},[170,726,712],{"class":224},[170,728,729],{"class":217}," \u002F 1000, 1)\"",[170,731,285],{"class":180},[170,733,734,736,738,741,743,745],{"class":172,"line":205},[170,735,303],{"class":217},[170,737,262],{"class":180},[170,739,740],{"class":217},"\"memory:km_posts_labelled\"",[170,742,316],{"class":180},[170,744,319],{"class":217},[170,746,227],{"class":180},[170,748,749,752,755,758,761,764,767],{"class":172,"line":230},[170,750,751],{"class":176},"for",[170,753,754],{"class":180}," f ",[170,756,757],{"class":176},"in",[170,759,760],{"class":224}," list",[170,762,763],{"class":180},"(labelled.getFeatures())[:",[170,765,766],{"class":224},"3",[170,768,769],{"class":180},"]:\n",[170,771,772,775,778,780,782,785,787,790],{"class":172,"line":247},[170,773,774],{"class":224},"    print",[170,776,777],{"class":180},"(f[",[170,779,678],{"class":217},[170,781,509],{"class":180},[170,783,784],{"class":224},"round",[170,786,777],{"class":180},[170,788,789],{"class":217},"\"angle\"",[170,791,792],{"class":180},"]))\n",[14,794,795,797,798,800,801,804,805,809],{},[360,796,362],{}," The label combines the route reference with the chainage in kilometres to one decimal — \"B7 km 3.0\". With the ",[18,799,370],{}," field, a marker symbol's rotation can be data-defined so it sits perpendicular to the road (",[18,802,803],{},"\"angle\" + 90","), the conventional look for kilometre posts. ",[26,806,808],{"href":807},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002F","Data-defined symbol properties"," explain wiring an attribute to rotation.",[133,811,813],{"id":812},"generate-points-yourself-for-special-rules","Generate points yourself for special rules",[14,815,816,817,819],{},"Processing covers fixed spacing. Other rules — exactly N points per line, points at given chainages, points that always include both ends — are easy with ",[18,818,20],{},", which returns the point at a given distance along a line.",[161,821,823],{"className":163,"code":822,"language":165,"meta":166,"style":166},"from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry\n\ndef points_at(line_geom, distances):\n    out = []\n    for d in distances:\n        p = line_geom.interpolate(d)\n        if not p.isNull():\n            angle = line_geom.interpolateAngle(d)\n            out.append((d, p, angle))\n    return out\n\nlayer = QgsVectorLayer(f\"Point?crs={pipes.crs().authid()}&field=pipe_id:string\"\n                       \"&field=chainage:double&field=angle_deg:double\", \"ends and quarters\", \"memory\")\nfeats = []\nfor f in pipes.getFeatures():\n    g = f.geometry()\n    L = g.length()\n    for d, p, a in points_at(g, [0, L * 0.25, L * 0.5, L * 0.75, L]):\n        nf = QgsFeature(layer.fields())\n        nf.setAttributes([f[\"pipe_id\"], round(d, 2), round(a * 180 \u002F 3.14159265, 1)])\n        nf.setGeometry(p)\n        feats.append(nf)\nlayer.dataProvider().addFeatures(feats)\n",[18,824,825,836,840,852,862,875,885,896,906,911,919,923,951,969,978,990,1001,1012,1053,1064,1109,1115,1121],{"__ignoreMap":166},[170,826,827,829,831,833],{"class":172,"line":173},[170,828,187],{"class":176},[170,830,190],{"class":180},[170,832,177],{"class":176},[170,834,835],{"class":180}," QgsVectorLayer, QgsFeature, QgsGeometry\n",[170,837,838],{"class":172,"line":184},[170,839,202],{"emptyLinePlaceholder":201},[170,841,842,845,849],{"class":172,"line":198},[170,843,844],{"class":176},"def",[170,846,848],{"class":847},"svObZ"," points_at",[170,850,851],{"class":180},"(line_geom, distances):\n",[170,853,854,857,859],{"class":172,"line":205},[170,855,856],{"class":180},"    out ",[170,858,211],{"class":176},[170,860,861],{"class":180}," []\n",[170,863,864,867,870,872],{"class":172,"line":230},[170,865,866],{"class":176},"    for",[170,868,869],{"class":180}," d ",[170,871,757],{"class":176},[170,873,874],{"class":180}," distances:\n",[170,876,877,880,882],{"class":172,"line":247},[170,878,879],{"class":180},"        p ",[170,881,211],{"class":176},[170,883,884],{"class":180}," line_geom.interpolate(d)\n",[170,886,887,890,893],{"class":172,"line":256},[170,888,889],{"class":176},"        if",[170,891,892],{"class":176}," not",[170,894,895],{"class":180}," p.isNull():\n",[170,897,898,901,903],{"class":172,"line":275},[170,899,900],{"class":180},"            angle ",[170,902,211],{"class":176},[170,904,905],{"class":180}," line_geom.interpolateAngle(d)\n",[170,907,908],{"class":172,"line":288},[170,909,910],{"class":180},"            out.append((d, p, angle))\n",[170,912,913,916],{"class":172,"line":300},[170,914,915],{"class":176},"    return",[170,917,918],{"class":180}," out\n",[170,920,921],{"class":172,"line":313},[170,922,202],{"emptyLinePlaceholder":201},[170,924,925,928,930,933,936,939,942,945,948],{"class":172,"line":324},[170,926,927],{"class":180},"layer ",[170,929,211],{"class":176},[170,931,932],{"class":180}," QgsVectorLayer(",[170,934,935],{"class":176},"f",[170,937,938],{"class":217},"\"Point?crs=",[170,940,941],{"class":224},"{",[170,943,944],{"class":180},"pipes.crs().authid()",[170,946,947],{"class":224},"}",[170,949,950],{"class":217},"&field=pipe_id:string\"\n",[170,952,953,956,958,961,963,966],{"class":172,"line":329},[170,954,955],{"class":217},"                       \"&field=chainage:double&field=angle_deg:double\"",[170,957,591],{"class":180},[170,959,960],{"class":217},"\"ends and quarters\"",[170,962,591],{"class":180},[170,964,965],{"class":217},"\"memory\"",[170,967,968],{"class":180},")\n",[170,970,971,974,976],{"class":172,"line":353},[170,972,973],{"class":180},"feats ",[170,975,211],{"class":176},[170,977,861],{"class":180},[170,979,981,983,985,987],{"class":172,"line":980},15,[170,982,751],{"class":176},[170,984,754],{"class":180},[170,986,757],{"class":176},[170,988,989],{"class":180}," pipes.getFeatures():\n",[170,991,993,996,998],{"class":172,"line":992},16,[170,994,995],{"class":180},"    g ",[170,997,211],{"class":176},[170,999,1000],{"class":180}," f.geometry()\n",[170,1002,1004,1007,1009],{"class":172,"line":1003},17,[170,1005,1006],{"class":180},"    L ",[170,1008,211],{"class":176},[170,1010,1011],{"class":180}," g.length()\n",[170,1013,1015,1017,1020,1022,1025,1027,1030,1033,1036,1038,1040,1043,1045,1047,1050],{"class":172,"line":1014},18,[170,1016,866],{"class":176},[170,1018,1019],{"class":180}," d, p, a ",[170,1021,757],{"class":176},[170,1023,1024],{"class":180}," points_at(g, [",[170,1026,51],{"class":224},[170,1028,1029],{"class":180},", L ",[170,1031,1032],{"class":176},"*",[170,1034,1035],{"class":224}," 0.25",[170,1037,1029],{"class":180},[170,1039,1032],{"class":176},[170,1041,1042],{"class":224}," 0.5",[170,1044,1029],{"class":180},[170,1046,1032],{"class":176},[170,1048,1049],{"class":224}," 0.75",[170,1051,1052],{"class":180},", L]):\n",[170,1054,1056,1059,1061],{"class":172,"line":1055},19,[170,1057,1058],{"class":180},"        nf ",[170,1060,211],{"class":176},[170,1062,1063],{"class":180}," QgsFeature(layer.fields())\n",[170,1065,1067,1070,1073,1075,1077,1080,1082,1085,1087,1090,1092,1095,1098,1101,1103,1106],{"class":172,"line":1066},20,[170,1068,1069],{"class":180},"        nf.setAttributes([f[",[170,1071,1072],{"class":217},"\"pipe_id\"",[170,1074,509],{"class":180},[170,1076,784],{"class":224},[170,1078,1079],{"class":180},"(d, ",[170,1081,347],{"class":224},[170,1083,1084],{"class":180},"), ",[170,1086,784],{"class":224},[170,1088,1089],{"class":180},"(a ",[170,1091,1032],{"class":176},[170,1093,1094],{"class":224}," 180",[170,1096,1097],{"class":176}," \u002F",[170,1099,1100],{"class":224}," 3.14159265",[170,1102,591],{"class":180},[170,1104,1105],{"class":224},"1",[170,1107,1108],{"class":180},")])\n",[170,1110,1112],{"class":172,"line":1111},21,[170,1113,1114],{"class":180},"        nf.setGeometry(p)\n",[170,1116,1118],{"class":172,"line":1117},22,[170,1119,1120],{"class":180},"        feats.append(nf)\n",[170,1122,1124],{"class":172,"line":1123},23,[170,1125,1126],{"class":180},"layer.dataProvider().addFeatures(feats)\n",[14,1128,1129,1131,1132,1135,1136,1139,1140,1143,1144,1148],{},[360,1130,362],{}," ",[18,1133,1134],{},"interpolate(d)"," walks the line from its start and returns the point at distance ",[18,1137,1138],{},"d","; ",[18,1141,1142],{},"interpolateAngle(d)"," returns the line's direction there in radians. Here every pipe gets five points — both ends and the quarter points — regardless of length, a rule Processing does not offer. The same function takes chainages from an inspection log to place recorded defects on the network, which is linear referencing in miniature; ",[26,1145,1147],{"href":1146},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis\u002F","linear referencing along lines"," covers the full technique, including measures stored on vertices.",[133,1150,1152],{"id":1151},"place-points-at-given-chainages-from-a-table","Place points at given chainages from a table",[14,1154,1155,1156,1159],{},"Field records often refer to positions by chainage: \"crack at km 4.35 on route B7\", \"valve at 1,280 m along main WM-112\". Turning such a table into points on the map is the reverse of generating chainage, and ",[18,1157,1158],{},"interpolate"," does it directly once each record is matched to its line.",[161,1161,1163],{"className":163,"code":1162,"language":165,"meta":166,"style":166},"import csv\n\nroutes_by_ref = {f[\"route_ref\"]: f.geometry() for f in merged.getFeatures()}\ndefects = QgsVectorLayer(f\"Point?crs={merged.crs().authid()}&field=route_ref:string\"\n                         \"&field=km:double&field=defect:string\", \"defects\", \"memory\")\nrows, missing = [], []\nwith open(\"\u002Fdata\u002Finspections\u002Fdefects.csv\", newline=\"\", encoding=\"utf-8\") as fh:\n    for r in csv.DictReader(fh):\n        line = routes_by_ref.get(r[\"route_ref\"])\n        km = float(r[\"km\"])\n        if line is None or km * 1000 > line.length():\n            missing.append(r)\n            continue\n        nf = QgsFeature(defects.fields())\n        nf.setAttributes([r[\"route_ref\"], km, r[\"defect\"]])\n        nf.setGeometry(line.interpolate(km * 1000))\n        rows.append(nf)\ndefects.dataProvider().addFeatures(rows)\nprint(len(rows), \"defects placed;\", len(missing), \"could not be located\")\n",[18,1164,1165,1172,1176,1200,1223,1239,1249,1293,1305,1320,1338,1368,1373,1378,1387,1403,1415,1420,1425],{"__ignoreMap":166},[170,1166,1167,1169],{"class":172,"line":173},[170,1168,177],{"class":176},[170,1170,1171],{"class":180}," csv\n",[170,1173,1174],{"class":172,"line":184},[170,1175,202],{"emptyLinePlaceholder":201},[170,1177,1178,1181,1183,1186,1188,1191,1193,1195,1197],{"class":172,"line":198},[170,1179,1180],{"class":180},"routes_by_ref ",[170,1182,211],{"class":176},[170,1184,1185],{"class":180}," {f[",[170,1187,506],{"class":217},[170,1189,1190],{"class":180},"]: f.geometry() ",[170,1192,751],{"class":176},[170,1194,754],{"class":180},[170,1196,757],{"class":176},[170,1198,1199],{"class":180}," merged.getFeatures()}\n",[170,1201,1202,1205,1207,1209,1211,1213,1215,1218,1220],{"class":172,"line":205},[170,1203,1204],{"class":180},"defects ",[170,1206,211],{"class":176},[170,1208,932],{"class":180},[170,1210,935],{"class":176},[170,1212,938],{"class":217},[170,1214,941],{"class":224},[170,1216,1217],{"class":180},"merged.crs().authid()",[170,1219,947],{"class":224},[170,1221,1222],{"class":217},"&field=route_ref:string\"\n",[170,1224,1225,1228,1230,1233,1235,1237],{"class":172,"line":230},[170,1226,1227],{"class":217},"                         \"&field=km:double&field=defect:string\"",[170,1229,591],{"class":180},[170,1231,1232],{"class":217},"\"defects\"",[170,1234,591],{"class":180},[170,1236,965],{"class":217},[170,1238,968],{"class":180},[170,1240,1241,1244,1246],{"class":172,"line":247},[170,1242,1243],{"class":180},"rows, missing ",[170,1245,211],{"class":176},[170,1247,1248],{"class":180}," [], []\n",[170,1250,1251,1254,1257,1260,1263,1265,1269,1271,1274,1276,1279,1281,1284,1287,1290],{"class":172,"line":256},[170,1252,1253],{"class":176},"with",[170,1255,1256],{"class":224}," open",[170,1258,1259],{"class":180},"(",[170,1261,1262],{"class":217},"\"\u002Fdata\u002Finspections\u002Fdefects.csv\"",[170,1264,591],{"class":180},[170,1266,1268],{"class":1267},"s9osk","newline",[170,1270,211],{"class":176},[170,1272,1273],{"class":217},"\"\"",[170,1275,591],{"class":180},[170,1277,1278],{"class":1267},"encoding",[170,1280,211],{"class":176},[170,1282,1283],{"class":217},"\"utf-8\"",[170,1285,1286],{"class":180},") ",[170,1288,1289],{"class":176},"as",[170,1291,1292],{"class":180}," fh:\n",[170,1294,1295,1297,1300,1302],{"class":172,"line":275},[170,1296,866],{"class":176},[170,1298,1299],{"class":180}," r ",[170,1301,757],{"class":176},[170,1303,1304],{"class":180}," csv.DictReader(fh):\n",[170,1306,1307,1310,1312,1315,1317],{"class":172,"line":288},[170,1308,1309],{"class":180},"        line ",[170,1311,211],{"class":176},[170,1313,1314],{"class":180}," routes_by_ref.get(r[",[170,1316,506],{"class":217},[170,1318,1319],{"class":180},"])\n",[170,1321,1322,1325,1327,1330,1333,1336],{"class":172,"line":300},[170,1323,1324],{"class":180},"        km ",[170,1326,211],{"class":176},[170,1328,1329],{"class":224}," float",[170,1331,1332],{"class":180},"(r[",[170,1334,1335],{"class":217},"\"km\"",[170,1337,1319],{"class":180},[170,1339,1340,1342,1345,1348,1351,1354,1357,1359,1362,1365],{"class":172,"line":313},[170,1341,889],{"class":176},[170,1343,1344],{"class":180}," line ",[170,1346,1347],{"class":176},"is",[170,1349,1350],{"class":224}," None",[170,1352,1353],{"class":176}," or",[170,1355,1356],{"class":180}," km ",[170,1358,1032],{"class":176},[170,1360,1361],{"class":224}," 1000",[170,1363,1364],{"class":176}," >",[170,1366,1367],{"class":180}," line.length():\n",[170,1369,1370],{"class":172,"line":324},[170,1371,1372],{"class":180},"            missing.append(r)\n",[170,1374,1375],{"class":172,"line":329},[170,1376,1377],{"class":176},"            continue\n",[170,1379,1380,1382,1384],{"class":172,"line":353},[170,1381,1058],{"class":180},[170,1383,211],{"class":176},[170,1385,1386],{"class":180}," QgsFeature(defects.fields())\n",[170,1388,1389,1392,1394,1397,1400],{"class":172,"line":980},[170,1390,1391],{"class":180},"        nf.setAttributes([r[",[170,1393,506],{"class":217},[170,1395,1396],{"class":180},"], km, r[",[170,1398,1399],{"class":217},"\"defect\"",[170,1401,1402],{"class":180},"]])\n",[170,1404,1405,1408,1410,1412],{"class":172,"line":992},[170,1406,1407],{"class":180},"        nf.setGeometry(line.interpolate(km ",[170,1409,1032],{"class":176},[170,1411,1361],{"class":224},[170,1413,1414],{"class":180},"))\n",[170,1416,1417],{"class":172,"line":1003},[170,1418,1419],{"class":180},"        rows.append(nf)\n",[170,1421,1422],{"class":172,"line":1014},[170,1423,1424],{"class":180},"defects.dataProvider().addFeatures(rows)\n",[170,1426,1427,1429,1431,1434,1437,1440,1442,1444,1447,1450],{"class":172,"line":1055},[170,1428,332],{"class":224},[170,1430,1259],{"class":180},[170,1432,1433],{"class":224},"len",[170,1435,1436],{"class":180},"(rows), ",[170,1438,1439],{"class":217},"\"defects placed;\"",[170,1441,591],{"class":180},[170,1443,1433],{"class":224},[170,1445,1446],{"class":180},"(missing), ",[170,1448,1449],{"class":217},"\"could not be located\"",[170,1451,968],{"class":180},[14,1453,1454,1456],{},[360,1455,362],{}," Looking routes up by reference in a dictionary keeps the loop fast. Records whose route does not exist, or whose chainage exceeds the route length, are collected rather than silently dropped — they usually point to a typo or a route that has been re-measured. This only works if chainage on the map matches chainage in the field, which is why merging segments into correctly oriented routes, as above, matters so much.",[133,1458,1460],{"id":1459},"sample-a-raster-along-the-points","Sample a raster along the points",[14,1462,1463],{},"A common reason to generate points is to read a raster at regular intervals: elevation along a river for a long profile, noise levels along a road, slope along a planned route.",[14,1465,1466],{},[34,1467,1470,1473,1476,1479,1482,1487,1489,1494,1498,1504],{"viewBox":1468,"role":37,"ariaLabel":1469,"xmlns":39},"0 0 760 230","Points along a river sampled against a DEM and plotted as elevation against chainage to form a long profile",[41,1471,1472],{},"From points to a profile",[45,1474,1475],{},"Points every 25 metres along a river carry chainage. Sampling a DEM at each point adds an elevation value. Plotting elevation against chainage gives a long profile showing the river's gradient, with steps at weirs.",[49,1477],{"x":51,"y":51,"width":52,"height":1478,"fill":54},"230",[56,1480,1481],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Chainage on x, sampled value on y",[172,1483],{"x1":1484,"y1":1485,"x2":123,"y2":1485,"stroke":125,"style":1486},"80","190","stroke-width:1.5",[172,1488],{"x1":1484,"y1":1485,"x2":1484,"y2":74,"stroke":125,"style":1486},[1490,1491],"polyline",{"points":1492,"fill":68,"stroke":1493,"style":70},"80,70 180,85 260,95 300,120 380,128 480,140 520,165 620,172 700,180","#0f766e",[56,1495,1497],{"x":93,"y":1496,"style":130,"fill":125,"textAnchor":62},"214","chainage (m)",[56,1499,1503],{"x":1500,"y":1501,"style":1502,"fill":125,"textAnchor":126},"72","66","text-anchor:end;font-size:10.0px;font-family:sans-serif","m",[56,1505,1508],{"x":1506,"y":1507,"style":1502,"fill":77,"textAnchor":126},"520","156","weir",[161,1510,1512],{"className":163,"code":1511,"language":165,"meta":166,"style":166},"river = QgsProject.instance().mapLayersByName(\"river_centreline\")[0]\npts = processing.run(\"native:pointsalonglines\", {\n    \"INPUT\": river, \"DISTANCE\": 25, \"START_OFFSET\": 0, \"END_OFFSET\": 0,\n    \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\nsampled = processing.run(\"native:rastersampling\", {\n    \"INPUT\": pts, \"RASTERCOPY\": \"\u002Fdata\u002Fdem\u002Fdem_1m.tif\", \"COLUMN_PREFIX\": \"z_\",\n    \"OUTPUT\": \"memory:river_profile\"})[\"OUTPUT\"]\n\nprofile = sorted((f[\"distance\"], f[\"z_1\"]) for f in sampled.getFeatures() if f[\"z_1\"] is not None)\ndrop = profile[0][1] - profile[-1][1]\nprint(f\"{len(profile)} samples, fall {drop:.1f} m over {profile[-1][0] \u002F 1000:.2f} km\")\n",[18,1513,1514,1532,1545,1577,1591,1605,1632,1647,1651,1704,1737],{"__ignoreMap":166},[170,1515,1516,1519,1521,1523,1526,1528,1530],{"class":172,"line":173},[170,1517,1518],{"class":180},"river ",[170,1520,211],{"class":176},[170,1522,214],{"class":180},[170,1524,1525],{"class":217},"\"river_centreline\"",[170,1527,221],{"class":180},[170,1529,51],{"class":224},[170,1531,227],{"class":180},[170,1533,1534,1537,1539,1541,1543],{"class":172,"line":184},[170,1535,1536],{"class":180},"pts ",[170,1538,211],{"class":176},[170,1540,238],{"class":180},[170,1542,241],{"class":217},[170,1544,244],{"class":180},[170,1546,1547,1549,1552,1554,1556,1559,1561,1563,1565,1567,1569,1571,1573,1575],{"class":172,"line":198},[170,1548,250],{"class":217},[170,1550,1551],{"class":180},": river, ",[170,1553,583],{"class":217},[170,1555,262],{"class":180},[170,1557,1558],{"class":224},"25",[170,1560,591],{"class":180},[170,1562,594],{"class":217},[170,1564,262],{"class":180},[170,1566,51],{"class":224},[170,1568,591],{"class":180},[170,1570,603],{"class":217},[170,1572,262],{"class":180},[170,1574,51],{"class":224},[170,1576,285],{"class":180},[170,1578,1579,1581,1583,1585,1587,1589],{"class":172,"line":205},[170,1580,303],{"class":217},[170,1582,262],{"class":180},[170,1584,516],{"class":217},[170,1586,316],{"class":180},[170,1588,319],{"class":217},[170,1590,227],{"class":180},[170,1592,1593,1596,1598,1600,1603],{"class":172,"line":230},[170,1594,1595],{"class":180},"sampled ",[170,1597,211],{"class":176},[170,1599,238],{"class":180},[170,1601,1602],{"class":217},"\"native:rastersampling\"",[170,1604,244],{"class":180},[170,1606,1607,1609,1612,1615,1617,1620,1622,1625,1627,1630],{"class":172,"line":247},[170,1608,250],{"class":217},[170,1610,1611],{"class":180},": pts, ",[170,1613,1614],{"class":217},"\"RASTERCOPY\"",[170,1616,262],{"class":180},[170,1618,1619],{"class":217},"\"\u002Fdata\u002Fdem\u002Fdem_1m.tif\"",[170,1621,591],{"class":180},[170,1623,1624],{"class":217},"\"COLUMN_PREFIX\"",[170,1626,262],{"class":180},[170,1628,1629],{"class":217},"\"z_\"",[170,1631,285],{"class":180},[170,1633,1634,1636,1638,1641,1643,1645],{"class":172,"line":256},[170,1635,303],{"class":217},[170,1637,262],{"class":180},[170,1639,1640],{"class":217},"\"memory:river_profile\"",[170,1642,316],{"class":180},[170,1644,319],{"class":217},[170,1646,227],{"class":180},[170,1648,1649],{"class":172,"line":275},[170,1650,202],{"emptyLinePlaceholder":201},[170,1652,1653,1656,1658,1661,1664,1667,1670,1673,1676,1678,1680,1682,1685,1688,1691,1693,1696,1698,1700,1702],{"class":172,"line":288},[170,1654,1655],{"class":180},"profile ",[170,1657,211],{"class":176},[170,1659,1660],{"class":224}," sorted",[170,1662,1663],{"class":180},"((f[",[170,1665,1666],{"class":217},"\"distance\"",[170,1668,1669],{"class":180},"], f[",[170,1671,1672],{"class":217},"\"z_1\"",[170,1674,1675],{"class":180},"]) ",[170,1677,751],{"class":176},[170,1679,754],{"class":180},[170,1681,757],{"class":176},[170,1683,1684],{"class":180}," sampled.getFeatures() ",[170,1686,1687],{"class":176},"if",[170,1689,1690],{"class":180}," f[",[170,1692,1672],{"class":217},[170,1694,1695],{"class":180},"] ",[170,1697,1347],{"class":176},[170,1699,892],{"class":176},[170,1701,1350],{"class":224},[170,1703,968],{"class":180},[170,1705,1706,1709,1711,1714,1716,1719,1721,1723,1725,1727,1729,1731,1733,1735],{"class":172,"line":300},[170,1707,1708],{"class":180},"drop ",[170,1710,211],{"class":176},[170,1712,1713],{"class":180}," profile[",[170,1715,51],{"class":224},[170,1717,1718],{"class":180},"][",[170,1720,1105],{"class":224},[170,1722,1695],{"class":180},[170,1724,344],{"class":176},[170,1726,1713],{"class":180},[170,1728,344],{"class":176},[170,1730,1105],{"class":224},[170,1732,1718],{"class":180},[170,1734,1105],{"class":224},[170,1736,227],{"class":180},[170,1738,1739,1741,1743,1745,1747,1750,1753,1755,1758,1760,1763,1766,1768,1771,1773,1776,1778,1780,1782,1784,1786,1789,1791,1794,1796,1799],{"class":172,"line":313},[170,1740,332],{"class":224},[170,1742,1259],{"class":180},[170,1744,935],{"class":176},[170,1746,709],{"class":217},[170,1748,1749],{"class":224},"{len",[170,1751,1752],{"class":180},"(profile)",[170,1754,947],{"class":224},[170,1756,1757],{"class":217}," samples, fall ",[170,1759,941],{"class":224},[170,1761,1762],{"class":180},"drop",[170,1764,1765],{"class":176},":.1f",[170,1767,947],{"class":224},[170,1769,1770],{"class":217}," m over ",[170,1772,941],{"class":224},[170,1774,1775],{"class":180},"profile[",[170,1777,344],{"class":176},[170,1779,1105],{"class":224},[170,1781,1718],{"class":180},[170,1783,51],{"class":224},[170,1785,1695],{"class":180},[170,1787,1788],{"class":176},"\u002F",[170,1790,1361],{"class":224},[170,1792,1793],{"class":176},":.2f",[170,1795,947],{"class":224},[170,1797,1798],{"class":217}," km\"",[170,1800,968],{"class":180},[14,1802,1803,1805,1806,1809,1810,1814,1815,1819],{},[360,1804,362],{}," Sampling adds one field per raster band holding the value at each point; ",[18,1807,1808],{},"z_1"," is band 1. Sorting by chainage gives the profile ready to plot, as in ",[26,1811,1813],{"href":1812},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fplot-layer-data-with-matplotlib-pyqgis\u002F","plotting layer data with matplotlib",". Spacing should match the raster: sampling a 25 m DEM every metre only repeats values. For a full-resolution profile of a single line, ",[26,1816,1818],{"href":1817},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fextract-elevation-profile-along-line-pyqgis\u002F","extracting an elevation profile"," reads every pixel the line crosses instead.",[133,1821,1823],{"id":1822},"points-at-line-ends-and-vertices","Points at line ends and vertices",[14,1825,1826],{},"Sometimes the points needed are not regular at all but structural: the start and end of every line (valves at pipe ends, junction candidates) or every vertex (survey points). Processing has dedicated algorithms for both.",[161,1828,1830],{"className":163,"code":1829,"language":165,"meta":166,"style":166},"ends = processing.run(\"native:extractspecificvertices\", {\n    \"INPUT\": pipes, \"VERTICES\": \"0,-1\", \"OUTPUT\": \"memory:pipe_ends\"})[\"OUTPUT\"]\nvertices = processing.run(\"native:extractvertices\", {\n    \"INPUT\": pipes, \"OUTPUT\": \"memory:pipe_vertices\"})[\"OUTPUT\"]\nprint(ends.featureCount(), \"end points;\", vertices.featureCount(), \"vertices\")\n",[18,1831,1832,1846,1876,1890,1909],{"__ignoreMap":166},[170,1833,1834,1837,1839,1841,1844],{"class":172,"line":173},[170,1835,1836],{"class":180},"ends ",[170,1838,211],{"class":176},[170,1840,238],{"class":180},[170,1842,1843],{"class":217},"\"native:extractspecificvertices\"",[170,1845,244],{"class":180},[170,1847,1848,1850,1853,1856,1858,1861,1863,1865,1867,1870,1872,1874],{"class":172,"line":184},[170,1849,250],{"class":217},[170,1851,1852],{"class":180},": pipes, ",[170,1854,1855],{"class":217},"\"VERTICES\"",[170,1857,262],{"class":180},[170,1859,1860],{"class":217},"\"0,-1\"",[170,1862,591],{"class":180},[170,1864,319],{"class":217},[170,1866,262],{"class":180},[170,1868,1869],{"class":217},"\"memory:pipe_ends\"",[170,1871,316],{"class":180},[170,1873,319],{"class":217},[170,1875,227],{"class":180},[170,1877,1878,1881,1883,1885,1888],{"class":172,"line":198},[170,1879,1880],{"class":180},"vertices ",[170,1882,211],{"class":176},[170,1884,238],{"class":180},[170,1886,1887],{"class":217},"\"native:extractvertices\"",[170,1889,244],{"class":180},[170,1891,1892,1894,1896,1898,1900,1903,1905,1907],{"class":172,"line":205},[170,1893,250],{"class":217},[170,1895,1852],{"class":180},[170,1897,319],{"class":217},[170,1899,262],{"class":180},[170,1901,1902],{"class":217},"\"memory:pipe_vertices\"",[170,1904,316],{"class":180},[170,1906,319],{"class":217},[170,1908,227],{"class":180},[170,1910,1911,1913,1916,1919,1922,1925],{"class":172,"line":230},[170,1912,332],{"class":224},[170,1914,1915],{"class":180},"(ends.featureCount(), ",[170,1917,1918],{"class":217},"\"end points;\"",[170,1920,1921],{"class":180},", vertices.featureCount(), ",[170,1923,1924],{"class":217},"\"vertices\"",[170,1926,968],{"class":180},[14,1928,1929,1131,1931,1934,1935,1938,1939,591,1942,1944,1945,637],{},[360,1930,362],{},[18,1932,1933],{},"native:extractspecificvertices"," takes a comma-separated list of vertex indices, where negative values count from the end — ",[18,1936,1937],{},"0,-1"," gives first and last. Both algorithms add ",[18,1940,1941],{},"vertex_index",[18,1943,366],{}," and angle fields, so each point knows where it sits on its line. End points are the input for connectivity checks such as ",[26,1946,1948],{"href":1947},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis\u002F","finding dangles in a network",[133,1950,1952],{"id":1951},"qgis-version-compatibility","QGIS version compatibility",[14,1954,1955,591,1957,591,1959,1961,1962,1965,1966,1961,1968,1971],{},[18,1956,158],{},[18,1958,632],{},[18,1960,1933],{}," and ",[18,1963,1964],{},"native:rastersampling"," are available with the same parameters on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. ",[18,1967,20],{},[18,1969,1970],{},"interpolateAngle"," have existed throughout QGIS 3.",[133,1973,1975],{"id":1974},"troubleshooting","Troubleshooting",[138,1977,1978,1984,1990,1996],{},[141,1979,1980,1983],{},[360,1981,1982],{},"Chainage restarts every few hundred metres."," The lines are split at junctions; dissolve and merge into routes first.",[141,1985,1986,1989],{},[360,1987,1988],{},"Points run in the wrong direction."," The lines were digitized backwards; reverse them.",[141,1991,1992,1995],{},[360,1993,1994],{},"Spacing looks wrong."," The layer is in degrees; reproject to a metric CRS.",[141,1997,1998,2001],{},[360,1999,2000],{},"Sampled values are NULL."," Points fall outside the raster or on NoData.",[133,2003,2005],{"id":2004},"conclusion","Conclusion",[14,2007,2008,2009,2011,2012,2014],{},"Use ",[18,2010,158],{}," for fixed spacing with offsets, merge segments into routes for continuous chainage, label with formatted kilometres and rotate symbols by angle, use ",[18,2013,1158],{}," for custom spacing rules, sample rasters at the points for profiles, and extract end points or vertices when structure rather than spacing matters.",[133,2016,2018],{"id":2017},"frequently-asked-questions","Frequently Asked Questions",[14,2020,2021,2024,2025,2028],{},[360,2022,2023],{},"Can spacing vary per line?","\nYes — make ",[18,2026,2027],{},"DISTANCE"," data-defined with an expression based on line attributes.",[14,2030,2031,2034,2035,2037,2038,2041,2042,637],{},[360,2032,2033],{},"How do I get exactly N points per line?","\nUse ",[18,2036,1158],{}," with distances ",[18,2039,2040],{},"length * i \u002F (N - 1)"," as shown, or a data-defined spacing of ",[18,2043,2044],{},"$length \u002F (N - 1)",[14,2046,2047,2050],{},[360,2048,2049],{},"Do points snap to vertices?","\nNo. They are placed at exact distances, usually between vertices.",[14,2052,2053,2056,2057,2060],{},[360,2054,2055],{},"Can I generate points along polygon outlines?","\nYes — convert polygons to lines with ",[18,2058,2059],{},"native:polygonstolines"," first.",[133,2062,2064],{"id":2063},"related","Related",[138,2066,2067,2072,2077,2083,2088],{},[141,2068,2069,2071],{},[26,2070,29],{"href":28}," — the guide this recipe belongs to",[141,2073,2074],{},[26,2075,2076],{"href":1146},"Linear Referencing Along Lines in PyQGIS",[141,2078,2079],{},[26,2080,2082],{"href":2081},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","Sample Raster Values at Points in PyQGIS",[141,2084,2085],{},[26,2086,2087],{"href":1817},"Extract an Elevation Profile Along a Line in PyQGIS",[141,2089,2090],{},[26,2091,2093],{"href":2092},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fcreate-centroids-and-bounding-geometries-pyqgis\u002F","Create Centroids and Bounding Geometries in PyQGIS",[2095,2096,2097],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":166,"searchDepth":184,"depth":184,"links":2099},[2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112],{"id":135,"depth":184,"text":136},{"id":152,"depth":184,"text":153},{"id":378,"depth":184,"text":379},{"id":640,"depth":184,"text":641},{"id":812,"depth":184,"text":813},{"id":1151,"depth":184,"text":1152},{"id":1459,"depth":184,"text":1460},{"id":1822,"depth":184,"text":1823},{"id":1951,"depth":184,"text":1952},{"id":1974,"depth":184,"text":1975},{"id":2004,"depth":184,"text":2005},{"id":2017,"depth":184,"text":2018},{"id":2063,"depth":184,"text":2064},"Place points at regular intervals along roads, rivers and pipes with native:pointsalonglines and QgsGeometry.interpolate — fixed spacing, start and end offsets, chainage and direction attributes, points at line ends, and sampling rasters along the generated points.","md",{"slug":2116,"type":2117,"breadcrumb":2118,"datePublished":2119,"dateModified":2119},"generate-points-along-lines-pyqgis","article","Generate Points Along Lines","2026-10-02","\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fgenerate-points-along-lines-pyqgis",{"title":5,"description":2113},"spatial-data-processing-automation\u002Fvector-data-manipulation\u002Fgenerate-points-along-lines-pyqgis\u002Findex","EXt6efqrp5v8STTDBvJeec3wsGysBcr5A4EYaNGtEUU",1790966265873]