[{"data":1,"prerenderedAt":2109},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis":3},{"id":4,"title":5,"body":6,"description":2098,"extension":2099,"meta":2100,"navigation":224,"path":2105,"seo":2106,"stem":2107,"__hash__":2108},"docs\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis\u002Findex.md","Linear Referencing Along Lines in PyQGIS",{"type":7,"value":8,"toc":2085},"minimark",[9,13,22,31,143,148,169,173,184,473,487,491,494,568,894,907,911,914,1049,1081,1085,1088,1192,1627,1645,1649,1652,1876,1902,1906,1951,1955,1992,1996,2008,2012,2022,2028,2036,2047,2051,2081],[10,11,5],"h1",{"id":12},"linear-referencing-along-lines-in-pyqgis",[14,15,16,17,21],"p",{},"Roads, rivers, pipelines, railways and power lines are managed by distance along them, not by coordinates. A pothole is at chainage 1,240 m on route A61; a culvert sits 3.2 km downstream of a gauging station; a speed restriction runs from kilometre 12.4 to 13.1. Converting between those positions and map geometry is ",[18,19,20],"em",{},"linear referencing",", and the QGIS geometry API has the primitives for it: place a point at a distance, measure the distance of a point, and cut the piece of line between two distances.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations and Spatial Predicates in PyQGIS",". It covers the three primitives, building an events table into geometry, generating regularly spaced points, and the checks for line direction and multipart geometry that decide whether a chainage means anything.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,85,91,97,101,105,111,117,123,130,134,138],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","The three linear referencing operations on one line: interpolate places a point at a distance, lineLocatePoint returns the distance of a nearby point, and a substring cuts the section between two distances","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Distance to point, point to distance, distances to section",[45,46,47],"desc",{},"A winding line from start at 0 m to end at 2,000 m. A green point sits at 600 m, placed by interpolate. An off-line orange point is projected onto the line at 1,150 m by lineLocatePoint, with a dashed perpendicular. A thick blue section runs from 1,400 m to 1,800 m, cut with a line substring.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Three questions about one line",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"24","44","712","236","10","#fffdf7","#59645f","stroke-width:2",[75,76],"path",{"d":77,"fill":78,"stroke":72,"style":79},"M60 220 C160 120 260 240 380 160 S560 90 700 130","none","stroke-width:5",[75,81],{"d":82,"fill":78,"stroke":83,"style":84},"M548 108 C600 96 650 112 690 128","#2563eb","stroke-width:10;opacity:0.75",[86,87],"circle",{"cx":88,"cy":89,"r":90,"fill":61},"60","220","6",[56,92,96],{"x":88,"y":93,"style":94,"fill":95,"textAnchor":62},"246","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","0 m",[86,98],{"cx":99,"cy":100,"r":90,"fill":61},"700","130",[56,102,104],{"x":99,"y":103,"style":94,"fill":95,"textAnchor":62},"156","2,000 m",[86,106],{"cx":107,"cy":108,"r":109,"fill":110},"222","178","8","#15803d",[56,112,116],{"x":107,"y":113,"style":114,"fill":115,"textAnchor":62},"210","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","#166534","interpolate(600)",[86,118],{"cx":119,"cy":120,"r":121,"fill":122},"440","206","7","#b45309",[124,125],"line",{"x1":119,"y1":126,"x2":127,"y2":128,"stroke":122,"style":129},"200","430","150","stroke-width:1.8;stroke-dasharray:4 3",[86,131],{"cx":127,"cy":132,"r":133,"fill":122},"146","4",[56,135,137],{"x":136,"y":69,"style":114,"fill":122,"textAnchor":62},"468","lineLocatePoint → 1,150",[56,139,142],{"x":140,"y":141,"style":114,"fill":83,"textAnchor":62},"618","80","substring 1,400 – 1,800",[144,145,147],"h2",{"id":146},"prerequisites","Prerequisites",[149,150,151,159,166],"ul",{},[152,153,154,158],"li",{},[155,156,157],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[152,160,161,162,165],{},"A line layer in a ",[155,163,164],{},"projected CRS"," whose units are metres. Distances along a line in degrees are not chainages.",[152,167,168],{},"Lines that are single-part and digitised in the direction chainage increases. The checks below confirm both.",[144,170,172],{"id":171},"place-a-point-at-a-distance","Place a point at a distance",[14,174,175,179,180,183],{},[176,177,178],"code",{},"QgsGeometry.interpolate(distance)"," returns a point geometry at that distance along the line, measured from the first vertex. ",[176,181,182],{},"interpolateAngle(distance)"," returns the line's direction there, which is useful for rotating symbols or offsetting perpendicular to the route.",[185,186,191],"pre",{"className":187,"code":188,"language":189,"meta":190,"style":190},"language-python shiki shiki-themes github-dark","import math\nfrom qgis.core import QgsProject, QgsGeometry, QgsPointXY\n\nroutes = QgsProject.instance().mapLayersByName(\"road_centrelines\")[0]\nroute = next(routes.getFeatures(\"\\\"route_id\\\" = 'A61'\"))\nline = route.geometry()\n\nprint(\"length:\", round(line.length(), 1), \"m\")\n\nchainage = 1240.0\npoint = line.interpolate(chainage)\nangle = line.interpolateAngle(chainage)\nprint(point.asPoint(), \"bearing\", round(math.degrees(angle), 1))\n\noffset = 4.0\nnormal = angle - math.pi \u002F 2\np = point.asPoint()\nkerb = QgsPointXY(p.x() + offset * math.sin(normal), p.y() + offset * math.cos(normal))\n","python","",[176,192,193,205,219,226,251,282,293,298,331,336,347,358,369,391,396,407,430,441],{"__ignoreMap":190},[194,195,197,201],"span",{"class":124,"line":196},1,[194,198,200],{"class":199},"snl16","import",[194,202,204],{"class":203},"s95oV"," math\n",[194,206,208,211,214,216],{"class":124,"line":207},2,[194,209,210],{"class":199},"from",[194,212,213],{"class":203}," qgis.core ",[194,215,200],{"class":199},[194,217,218],{"class":203}," QgsProject, QgsGeometry, QgsPointXY\n",[194,220,222],{"class":124,"line":221},3,[194,223,225],{"emptyLinePlaceholder":224},true,"\n",[194,227,229,232,235,238,242,245,248],{"class":124,"line":228},4,[194,230,231],{"class":203},"routes ",[194,233,234],{"class":199},"=",[194,236,237],{"class":203}," QgsProject.instance().mapLayersByName(",[194,239,241],{"class":240},"sU2Wk","\"road_centrelines\"",[194,243,244],{"class":203},")[",[194,246,51],{"class":247},"sDLfK",[194,249,250],{"class":203},"]\n",[194,252,254,257,259,262,265,268,271,274,276,279],{"class":124,"line":253},5,[194,255,256],{"class":203},"route ",[194,258,234],{"class":199},[194,260,261],{"class":247}," next",[194,263,264],{"class":203},"(routes.getFeatures(",[194,266,267],{"class":240},"\"",[194,269,270],{"class":247},"\\\"",[194,272,273],{"class":240},"route_id",[194,275,270],{"class":247},[194,277,278],{"class":240}," = 'A61'\"",[194,280,281],{"class":203},"))\n",[194,283,285,288,290],{"class":124,"line":284},6,[194,286,287],{"class":203},"line ",[194,289,234],{"class":199},[194,291,292],{"class":203}," route.geometry()\n",[194,294,296],{"class":124,"line":295},7,[194,297,225],{"emptyLinePlaceholder":224},[194,299,301,304,307,310,313,316,319,322,325,328],{"class":124,"line":300},8,[194,302,303],{"class":247},"print",[194,305,306],{"class":203},"(",[194,308,309],{"class":240},"\"length:\"",[194,311,312],{"class":203},", ",[194,314,315],{"class":247},"round",[194,317,318],{"class":203},"(line.length(), ",[194,320,321],{"class":247},"1",[194,323,324],{"class":203},"), ",[194,326,327],{"class":240},"\"m\"",[194,329,330],{"class":203},")\n",[194,332,334],{"class":124,"line":333},9,[194,335,225],{"emptyLinePlaceholder":224},[194,337,339,342,344],{"class":124,"line":338},10,[194,340,341],{"class":203},"chainage ",[194,343,234],{"class":199},[194,345,346],{"class":247}," 1240.0\n",[194,348,350,353,355],{"class":124,"line":349},11,[194,351,352],{"class":203},"point ",[194,354,234],{"class":199},[194,356,357],{"class":203}," line.interpolate(chainage)\n",[194,359,361,364,366],{"class":124,"line":360},12,[194,362,363],{"class":203},"angle ",[194,365,234],{"class":199},[194,367,368],{"class":203}," line.interpolateAngle(chainage)\n",[194,370,372,374,377,380,382,384,387,389],{"class":124,"line":371},13,[194,373,303],{"class":247},[194,375,376],{"class":203},"(point.asPoint(), ",[194,378,379],{"class":240},"\"bearing\"",[194,381,312],{"class":203},[194,383,315],{"class":247},[194,385,386],{"class":203},"(math.degrees(angle), ",[194,388,321],{"class":247},[194,390,281],{"class":203},[194,392,394],{"class":124,"line":393},14,[194,395,225],{"emptyLinePlaceholder":224},[194,397,399,402,404],{"class":124,"line":398},15,[194,400,401],{"class":203},"offset ",[194,403,234],{"class":199},[194,405,406],{"class":247}," 4.0\n",[194,408,410,413,415,418,421,424,427],{"class":124,"line":409},16,[194,411,412],{"class":203},"normal ",[194,414,234],{"class":199},[194,416,417],{"class":203}," angle ",[194,419,420],{"class":199},"-",[194,422,423],{"class":203}," math.pi ",[194,425,426],{"class":199},"\u002F",[194,428,429],{"class":247}," 2\n",[194,431,433,436,438],{"class":124,"line":432},17,[194,434,435],{"class":203},"p ",[194,437,234],{"class":199},[194,439,440],{"class":203}," point.asPoint()\n",[194,442,444,447,449,452,455,458,461,464,466,468,470],{"class":124,"line":443},18,[194,445,446],{"class":203},"kerb ",[194,448,234],{"class":199},[194,450,451],{"class":203}," QgsPointXY(p.x() ",[194,453,454],{"class":199},"+",[194,456,457],{"class":203}," offset ",[194,459,460],{"class":199},"*",[194,462,463],{"class":203}," math.sin(normal), p.y() ",[194,465,454],{"class":199},[194,467,457],{"class":203},[194,469,460],{"class":199},[194,471,472],{"class":203}," math.cos(normal))\n",[14,474,475,478,479,482,483,486],{},[155,476,477],{},"Breakdown:"," ",[176,480,481],{},"interpolate"," returns an empty geometry — not an error — when the distance is negative or longer than the line, so check ",[176,484,485],{},"point.isEmpty()"," when chainages come from outside data. The angle is in radians, clockwise from north, which is the convention map symbol rotation also uses. The perpendicular offset turns a chainage into a position on the left of the direction of travel: four metres to the left places an event at the kerb rather than on the centreline, which matters when a survey records which side of the road an asset is on. Subtract the right angle for the left side and add it for the right.",[144,488,490],{"id":489},"measure-how-far-along-a-point-lies","Measure how far along a point lies",[14,492,493],{},"The reverse operation projects a point onto the nearest position on the line and returns the distance from the start. Points need not lie on the line; the result is the distance of their perpendicular foot.",[14,495,496],{},[34,497,500,503,506,509,513,520,526,529,534,537,540,544,548,552,555,559,562,565],{"viewBox":498,"role":37,"ariaLabel":499,"xmlns":39},"0 0 760 256","Why lineLocatePoint needs the right line: a point near a junction projects onto the nearest line, which may be the side road rather than the route, so the candidate line must be chosen by route id before measuring",[41,501,502],{},"Measure against the right route",[45,504,505],{},"A main road runs left to right with a side road branching off. An asset point sits between them, slightly closer to the side road. Projecting against the nearest line gives a chainage on the side road, which is wrong. Filtering to the route the asset belongs to, then projecting, gives the correct chainage on the main road, along with the offset distance to check.",[49,507],{"x":51,"y":51,"width":52,"height":508,"fill":54},"256",[56,510,512],{"x":58,"y":511,"style":60,"fill":61,"textAnchor":62},"26","Nearest line is not always the asset’s line",[49,514],{"x":66,"y":67,"width":515,"height":516,"rx":70,"fill":517,"stroke":518,"style":519},"344","192","#fdf2e2","#b91c1c","stroke-width:2.5",[56,521,525],{"x":522,"y":523,"style":524,"fill":518,"textAnchor":62},"196","68","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","nearest line",[75,527],{"d":528,"fill":78,"stroke":72,"style":79},"M44 170 L348 170",[75,530],{"d":531,"fill":78,"stroke":532,"style":533},"M200 170 L300 90","#9aa39d","stroke-width:4",[86,535],{"cx":69,"cy":536,"r":90,"fill":122},"152",[124,538],{"x1":69,"y1":536,"x2":539,"y2":100,"stroke":518,"style":129},"248",[56,541,543],{"x":522,"y":542,"style":94,"fill":518,"textAnchor":62},"212","measured on the side road",[49,545],{"x":546,"y":67,"width":515,"height":516,"rx":70,"fill":547,"stroke":110,"style":519},"392","#e8efe6",[56,549,551],{"x":550,"y":523,"style":524,"fill":115,"textAnchor":62},"564","route chosen by id",[75,553],{"d":554,"fill":78,"stroke":110,"style":79},"M412 170 L716 170",[75,556],{"d":557,"fill":78,"stroke":558,"style":533},"M568 170 L668 90","#c9c2ac",[86,560],{"cx":561,"cy":536,"r":90,"fill":122},"604",[124,563],{"x1":561,"y1":536,"x2":561,"y2":564,"stroke":110,"style":129},"170",[56,566,567],{"x":550,"y":542,"style":94,"fill":115,"textAnchor":62},"chainage on A61, offset 18 m",[185,569,571],{"className":187,"code":570,"language":189,"meta":190,"style":190},"assets = QgsProject.instance().mapLayersByName(\"drainage_assets\")[0]\n\nroute_lines = {\n    f[\"route_id\"]: f.geometry() for f in routes.getFeatures()\n}\n\nresults = []\nfor asset in assets.getFeatures():\n    line = route_lines.get(asset[\"route_id\"])\n    if line is None:\n        continue\n    pt = asset.geometry()\n    distance_along = line.lineLocatePoint(pt)\n    foot = line.interpolate(distance_along)\n    offset = pt.distance(foot)\n    results.append((asset[\"asset_ref\"], round(distance_along, 1), round(offset, 1)))\n\nfor ref, ch, off in sorted(results, key=lambda r: r[1]):\n    flag = \"  CHECK\" if off > 25 else \"\"\n    print(f\"{ref:\u003C10} ch {ch:>8} m  offset {off:>5} m{flag}\")\n",[176,572,573,591,595,605,628,633,637,647,659,674,691,696,706,716,726,736,766,770,800,829],{"__ignoreMap":190},[194,574,575,578,580,582,585,587,589],{"class":124,"line":196},[194,576,577],{"class":203},"assets ",[194,579,234],{"class":199},[194,581,237],{"class":203},[194,583,584],{"class":240},"\"drainage_assets\"",[194,586,244],{"class":203},[194,588,51],{"class":247},[194,590,250],{"class":203},[194,592,593],{"class":124,"line":207},[194,594,225],{"emptyLinePlaceholder":224},[194,596,597,600,602],{"class":124,"line":221},[194,598,599],{"class":203},"route_lines ",[194,601,234],{"class":199},[194,603,604],{"class":203}," {\n",[194,606,607,610,613,616,619,622,625],{"class":124,"line":228},[194,608,609],{"class":203},"    f[",[194,611,612],{"class":240},"\"route_id\"",[194,614,615],{"class":203},"]: f.geometry() ",[194,617,618],{"class":199},"for",[194,620,621],{"class":203}," f ",[194,623,624],{"class":199},"in",[194,626,627],{"class":203}," routes.getFeatures()\n",[194,629,630],{"class":124,"line":253},[194,631,632],{"class":203},"}\n",[194,634,635],{"class":124,"line":284},[194,636,225],{"emptyLinePlaceholder":224},[194,638,639,642,644],{"class":124,"line":295},[194,640,641],{"class":203},"results ",[194,643,234],{"class":199},[194,645,646],{"class":203}," []\n",[194,648,649,651,654,656],{"class":124,"line":300},[194,650,618],{"class":199},[194,652,653],{"class":203}," asset ",[194,655,624],{"class":199},[194,657,658],{"class":203}," assets.getFeatures():\n",[194,660,661,664,666,669,671],{"class":124,"line":333},[194,662,663],{"class":203},"    line ",[194,665,234],{"class":199},[194,667,668],{"class":203}," route_lines.get(asset[",[194,670,612],{"class":240},[194,672,673],{"class":203},"])\n",[194,675,676,679,682,685,688],{"class":124,"line":338},[194,677,678],{"class":199},"    if",[194,680,681],{"class":203}," line ",[194,683,684],{"class":199},"is",[194,686,687],{"class":247}," None",[194,689,690],{"class":203},":\n",[194,692,693],{"class":124,"line":349},[194,694,695],{"class":199},"        continue\n",[194,697,698,701,703],{"class":124,"line":360},[194,699,700],{"class":203},"    pt ",[194,702,234],{"class":199},[194,704,705],{"class":203}," asset.geometry()\n",[194,707,708,711,713],{"class":124,"line":371},[194,709,710],{"class":203},"    distance_along ",[194,712,234],{"class":199},[194,714,715],{"class":203}," line.lineLocatePoint(pt)\n",[194,717,718,721,723],{"class":124,"line":393},[194,719,720],{"class":203},"    foot ",[194,722,234],{"class":199},[194,724,725],{"class":203}," line.interpolate(distance_along)\n",[194,727,728,731,733],{"class":124,"line":398},[194,729,730],{"class":203},"    offset ",[194,732,234],{"class":199},[194,734,735],{"class":203}," pt.distance(foot)\n",[194,737,738,741,744,747,749,752,754,756,758,761,763],{"class":124,"line":409},[194,739,740],{"class":203},"    results.append((asset[",[194,742,743],{"class":240},"\"asset_ref\"",[194,745,746],{"class":203},"], ",[194,748,315],{"class":247},[194,750,751],{"class":203},"(distance_along, ",[194,753,321],{"class":247},[194,755,324],{"class":203},[194,757,315],{"class":247},[194,759,760],{"class":203},"(offset, ",[194,762,321],{"class":247},[194,764,765],{"class":203},")))\n",[194,767,768],{"class":124,"line":432},[194,769,225],{"emptyLinePlaceholder":224},[194,771,772,774,777,779,782,785,789,792,795,797],{"class":124,"line":443},[194,773,618],{"class":199},[194,775,776],{"class":203}," ref, ch, off ",[194,778,624],{"class":199},[194,780,781],{"class":247}," sorted",[194,783,784],{"class":203},"(results, ",[194,786,788],{"class":787},"s9osk","key",[194,790,791],{"class":199},"=lambda",[194,793,794],{"class":203}," r: r[",[194,796,321],{"class":247},[194,798,799],{"class":203},"]):\n",[194,801,803,806,808,811,814,817,820,823,826],{"class":124,"line":802},19,[194,804,805],{"class":203},"    flag ",[194,807,234],{"class":199},[194,809,810],{"class":240}," \"  CHECK\"",[194,812,813],{"class":199}," if",[194,815,816],{"class":203}," off ",[194,818,819],{"class":199},">",[194,821,822],{"class":247}," 25",[194,824,825],{"class":199}," else",[194,827,828],{"class":240}," \"\"\n",[194,830,832,835,837,840,842,845,848,851,854,857,859,862,865,867,870,872,875,878,880,883,885,888,890,892],{"class":124,"line":831},20,[194,833,834],{"class":247},"    print",[194,836,306],{"class":203},[194,838,839],{"class":199},"f",[194,841,267],{"class":240},[194,843,844],{"class":247},"{",[194,846,847],{"class":203},"ref",[194,849,850],{"class":199},":\u003C10",[194,852,853],{"class":247},"}",[194,855,856],{"class":240}," ch ",[194,858,844],{"class":247},[194,860,861],{"class":203},"ch",[194,863,864],{"class":199},":>8",[194,866,853],{"class":247},[194,868,869],{"class":240}," m  offset ",[194,871,844],{"class":247},[194,873,874],{"class":203},"off",[194,876,877],{"class":199},":>5",[194,879,853],{"class":247},[194,881,882],{"class":240}," m",[194,884,844],{"class":247},[194,886,887],{"class":203},"flag",[194,889,853],{"class":247},[194,891,267],{"class":240},[194,893,330],{"class":203},[14,895,896,898,899,902,903,906],{},[155,897,477],{}," Matching each asset to its own route by identifier, rather than to whichever line is nearest, is what keeps an asset at a junction on the right road; the nearest-line approach is only safe when routes never come close. ",[176,900,901],{},"lineLocatePoint"," returns ",[176,904,905],{},"-1"," on failure, typically for an empty or non-line geometry. Interpolating back to the foot and measuring the offset is the quality check: an asset tens of metres from its route has the wrong route id or the wrong coordinates, and a chainage computed for it is fiction. Sorting by chainage gives the order a maintenance crew would drive.",[144,908,910],{"id":909},"cut-the-section-between-two-chainages","Cut the section between two chainages",[14,912,913],{},"Linear events — a speed limit, a resurfacing scheme, a length of pipe of one material — are stored as a start and end distance. Turning them into line geometry means cutting the substring between those distances.",[185,915,917],{"className":187,"code":916,"language":189,"meta":190,"style":190},"def section(line_geom, start, end):\n    if line_geom.isMultipart():\n        raise ValueError(\"multipart line; merge or explode first\")\n    start, end = sorted((max(0.0, start), min(line_geom.length(), end)))\n    return QgsGeometry(line_geom.constGet().curveSubstring(start, end))\n\na61 = route_lines[\"A61\"]\nrestriction = section(a61, 12400, 13100)\nprint(\"section length:\", round(restriction.length(), 1))\n",[176,918,919,931,938,953,982,990,994,1009,1029],{"__ignoreMap":190},[194,920,921,924,928],{"class":124,"line":196},[194,922,923],{"class":199},"def",[194,925,927],{"class":926},"svObZ"," section",[194,929,930],{"class":203},"(line_geom, start, end):\n",[194,932,933,935],{"class":124,"line":207},[194,934,678],{"class":199},[194,936,937],{"class":203}," line_geom.isMultipart():\n",[194,939,940,943,946,948,951],{"class":124,"line":221},[194,941,942],{"class":199},"        raise",[194,944,945],{"class":247}," ValueError",[194,947,306],{"class":203},[194,949,950],{"class":240},"\"multipart line; merge or explode first\"",[194,952,330],{"class":203},[194,954,955,958,960,962,965,968,970,973,976,979],{"class":124,"line":228},[194,956,957],{"class":203},"    start, end ",[194,959,234],{"class":199},[194,961,781],{"class":247},[194,963,964],{"class":203},"((",[194,966,967],{"class":247},"max",[194,969,306],{"class":203},[194,971,972],{"class":247},"0.0",[194,974,975],{"class":203},", start), ",[194,977,978],{"class":247},"min",[194,980,981],{"class":203},"(line_geom.length(), end)))\n",[194,983,984,987],{"class":124,"line":253},[194,985,986],{"class":199},"    return",[194,988,989],{"class":203}," QgsGeometry(line_geom.constGet().curveSubstring(start, end))\n",[194,991,992],{"class":124,"line":284},[194,993,225],{"emptyLinePlaceholder":224},[194,995,996,999,1001,1004,1007],{"class":124,"line":295},[194,997,998],{"class":203},"a61 ",[194,1000,234],{"class":199},[194,1002,1003],{"class":203}," route_lines[",[194,1005,1006],{"class":240},"\"A61\"",[194,1008,250],{"class":203},[194,1010,1011,1014,1016,1019,1022,1024,1027],{"class":124,"line":300},[194,1012,1013],{"class":203},"restriction ",[194,1015,234],{"class":199},[194,1017,1018],{"class":203}," section(a61, ",[194,1020,1021],{"class":247},"12400",[194,1023,312],{"class":203},[194,1025,1026],{"class":247},"13100",[194,1028,330],{"class":203},[194,1030,1031,1033,1035,1038,1040,1042,1045,1047],{"class":124,"line":333},[194,1032,303],{"class":247},[194,1034,306],{"class":203},[194,1036,1037],{"class":240},"\"section length:\"",[194,1039,312],{"class":203},[194,1041,315],{"class":247},[194,1043,1044],{"class":203},"(restriction.length(), ",[194,1046,321],{"class":247},[194,1048,281],{"class":203},[14,1050,1051,478,1053,1056,1057,1060,1061,1064,1065,1068,1069,1072,1073,1076,1077,1080],{},[155,1052,477],{},[176,1054,1055],{},"curveSubstring"," is a method of the underlying curve object (",[176,1058,1059],{},"QgsLineString"," or ",[176,1062,1063],{},"QgsCompoundCurve","), which is why the code reaches through ",[176,1066,1067],{},"constGet()","; wrapping the result in ",[176,1070,1071],{},"QgsGeometry"," gives back the familiar API. Clamping to the line length stops an events table with a slightly overlong end chainage — common when chainages were measured on an older version of the centreline — from producing an empty result. Sorting the two distances tolerates events recorded against the direction of the line. The multipart check matters because a substring of a ",[176,1074,1075],{},"MultiLineString"," is undefined: merge contiguous parts with ",[176,1078,1079],{},"lineMerge()"," first, or split the route.",[144,1082,1084],{"id":1083},"build-geometry-from-an-events-table","Build geometry from an events table",[14,1086,1087],{},"The usual input is a table — a spreadsheet or database table with a route id, a start and end chainage, and attributes. The Processing algorithm does the substring for every row, but a short loop with the functions above gives you control over clamping and reporting.",[14,1089,1090],{},[34,1091,1094,1097,1100,1103,1116,1119,1123,1129,1135,1139,1142,1146,1152,1157,1161,1164,1168,1172,1177,1181,1185,1188],{"viewBox":1092,"role":37,"ariaLabel":1093,"xmlns":39},"0 0 760 250","Converting an events table to geometry: each row's route id selects a centreline, the start and end chainages cut a substring, and the result becomes a line feature carrying the row's attributes, with out-of-range rows reported",[41,1095,1096],{},"From rows of chainages to line features",[45,1098,1099],{},"A table with three rows: A61 from 12,400 to 13,100 with limit 30, A61 from 15,000 to 15,800 with limit 40, and B6154 from 200 to 26,000 with limit 20. The first two become line features on route A61. The third has an end chainage beyond the route's 4,950 m length, so it is clamped and reported for review.",[49,1101],{"x":51,"y":51,"width":52,"height":1102,"fill":54},"250",[1104,1105,1106],"defs",{},[1107,1108,1113],"marker",{"id":1109,"viewBox":1110,"refX":109,"refY":1111,"markerWidth":121,"markerHeight":121,"orient":1112},"lrEvArrow","0 0 10 10","5","auto-start-reverse",[75,1114],{"d":1115,"fill":95},"M0 0 L10 5 L0 10 z",[56,1117,1118],{"x":58,"y":511,"style":60,"fill":61,"textAnchor":62},"Every row becomes a section, or a report line",[49,1120],{"x":66,"y":1121,"width":53,"height":1122,"rx":70,"fill":71,"stroke":72,"style":73},"48","180",[56,1124,1128],{"x":1125,"y":1126,"style":1127,"fill":61,"textAnchor":62},"174","72","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","speed_limits.csv",[56,1130,1134],{"x":1131,"y":1132,"style":1133,"fill":72},"40","102","font-size:9.5px;font-family:monospace","route  start   end    mph",[56,1136,1138],{"x":1131,"y":1137,"style":1133,"fill":95},"128","A61    12400   13100  30",[56,1140,1141],{"x":1131,"y":536,"style":1133,"fill":95},"A61    15000   15800  40",[56,1143,1145],{"x":1131,"y":1144,"style":1133,"fill":518},"176","B6154  200     26000  20",[124,1147],{"x1":1148,"y1":1149,"x2":1150,"y2":1149,"stroke":95,"style":1151},"324","138","376","stroke-width:1.8;marker-end:url(#lrEvArrow)",[49,1153],{"x":1154,"y":1121,"width":1155,"height":1156,"rx":70,"fill":547,"stroke":110,"style":73},"384","352","112",[56,1158,1160],{"x":1159,"y":1126,"style":1127,"fill":115,"textAnchor":62},"560","2 line features",[75,1162],{"d":1163,"fill":78,"stroke":72,"style":533},"M404 130 L716 130",[75,1165],{"d":1166,"fill":78,"stroke":83,"style":1167},"M500 130 L560 130","stroke-width:10",[75,1169],{"d":1170,"fill":78,"stroke":1171,"style":1167},"M620 130 L680 130","#0f766e",[56,1173,1176],{"x":1174,"y":1156,"style":1175,"fill":95,"textAnchor":62},"530","text-anchor:middle;font-size:9.5px;font-family:sans-serif","30 mph",[56,1178,1180],{"x":1179,"y":1156,"style":1175,"fill":95,"textAnchor":62},"650","40 mph",[49,1182],{"x":1154,"y":1183,"width":1155,"height":1184,"rx":70,"fill":517,"stroke":518,"style":73},"172","56",[56,1186,1187],{"x":1159,"y":522,"style":114,"fill":518,"textAnchor":62},"1 clamped and reported",[56,1189,1191],{"x":1159,"y":1190,"style":94,"fill":95,"textAnchor":62},"216","end 26,000 m beyond route length 4,950 m",[185,1193,1195],{"className":187,"code":1194,"language":189,"meta":190,"style":190},"from qgis.core import QgsVectorLayer, QgsFeature, QgsField, QgsFields\nfrom qgis.PyQt.QtCore import QMetaType\n\nevents = QgsProject.instance().mapLayersByName(\"speed_limits\")[0]   # geometryless table\n\nout = QgsVectorLayer(f\"LineString?crs={routes.crs().authid()}\", \"speed_limit_sections\", \"memory\")\nout_fields = QgsFields()\nfor name, kind in ((\"route_id\", QMetaType.Type.QString), (\"start_m\", QMetaType.Type.Double),\n                   (\"end_m\", QMetaType.Type.Double), (\"mph\", QMetaType.Type.Int)):\n    out_fields.append(QgsField(name, kind))\nout.dataProvider().addAttributes(out_fields)\nout.updateFields()\n\nfeatures, report = [], []\nfor ev in events.getFeatures():\n    line = route_lines.get(ev[\"route_id\"])\n    if line is None:\n        report.append(f\"{ev['route_id']}: no such route\")\n        continue\n    if ev[\"end_m\"] > line.length() + 1:\n        report.append(f\"{ev['route_id']} {ev['start_m']}–{ev['end_m']}: \"\n                      f\"beyond length {line.length():.0f}\")\n    f = QgsFeature(out_fields)\n    f.setGeometry(section(line, ev[\"start_m\"], ev[\"end_m\"]))\n    f.setAttributes([ev[\"route_id\"], ev[\"start_m\"], ev[\"end_m\"], ev[\"mph\"]])\n    features.append(f)\n\nout.dataProvider().addFeatures(features)\nQgsProject.instance().addMapLayer(out)\nprint(\"\\n\".join(report) or \"all events within route lengths\")\n",[176,1196,1197,1208,1220,1224,1247,1251,1287,1297,1320,1337,1342,1347,1352,1356,1366,1378,1391,1403,1430,1434,1458,1506,1529,1540,1556,1579,1585,1590,1596,1602],{"__ignoreMap":190},[194,1198,1199,1201,1203,1205],{"class":124,"line":196},[194,1200,210],{"class":199},[194,1202,213],{"class":203},[194,1204,200],{"class":199},[194,1206,1207],{"class":203}," QgsVectorLayer, QgsFeature, QgsField, QgsFields\n",[194,1209,1210,1212,1215,1217],{"class":124,"line":207},[194,1211,210],{"class":199},[194,1213,1214],{"class":203}," qgis.PyQt.QtCore ",[194,1216,200],{"class":199},[194,1218,1219],{"class":203}," QMetaType\n",[194,1221,1222],{"class":124,"line":221},[194,1223,225],{"emptyLinePlaceholder":224},[194,1225,1226,1229,1231,1233,1236,1238,1240,1243],{"class":124,"line":228},[194,1227,1228],{"class":203},"events ",[194,1230,234],{"class":199},[194,1232,237],{"class":203},[194,1234,1235],{"class":240},"\"speed_limits\"",[194,1237,244],{"class":203},[194,1239,51],{"class":247},[194,1241,1242],{"class":203},"]   ",[194,1244,1246],{"class":1245},"sjoCn","# geometryless table\n",[194,1248,1249],{"class":124,"line":253},[194,1250,225],{"emptyLinePlaceholder":224},[194,1252,1253,1256,1258,1261,1263,1266,1268,1271,1273,1275,1277,1280,1282,1285],{"class":124,"line":284},[194,1254,1255],{"class":203},"out ",[194,1257,234],{"class":199},[194,1259,1260],{"class":203}," QgsVectorLayer(",[194,1262,839],{"class":199},[194,1264,1265],{"class":240},"\"LineString?crs=",[194,1267,844],{"class":247},[194,1269,1270],{"class":203},"routes.crs().authid()",[194,1272,853],{"class":247},[194,1274,267],{"class":240},[194,1276,312],{"class":203},[194,1278,1279],{"class":240},"\"speed_limit_sections\"",[194,1281,312],{"class":203},[194,1283,1284],{"class":240},"\"memory\"",[194,1286,330],{"class":203},[194,1288,1289,1292,1294],{"class":124,"line":295},[194,1290,1291],{"class":203},"out_fields ",[194,1293,234],{"class":199},[194,1295,1296],{"class":203}," QgsFields()\n",[194,1298,1299,1301,1304,1306,1309,1311,1314,1317],{"class":124,"line":300},[194,1300,618],{"class":199},[194,1302,1303],{"class":203}," name, kind ",[194,1305,624],{"class":199},[194,1307,1308],{"class":203}," ((",[194,1310,612],{"class":240},[194,1312,1313],{"class":203},", QMetaType.Type.QString), (",[194,1315,1316],{"class":240},"\"start_m\"",[194,1318,1319],{"class":203},", QMetaType.Type.Double),\n",[194,1321,1322,1325,1328,1331,1334],{"class":124,"line":333},[194,1323,1324],{"class":203},"                   (",[194,1326,1327],{"class":240},"\"end_m\"",[194,1329,1330],{"class":203},", QMetaType.Type.Double), (",[194,1332,1333],{"class":240},"\"mph\"",[194,1335,1336],{"class":203},", QMetaType.Type.Int)):\n",[194,1338,1339],{"class":124,"line":338},[194,1340,1341],{"class":203},"    out_fields.append(QgsField(name, kind))\n",[194,1343,1344],{"class":124,"line":349},[194,1345,1346],{"class":203},"out.dataProvider().addAttributes(out_fields)\n",[194,1348,1349],{"class":124,"line":360},[194,1350,1351],{"class":203},"out.updateFields()\n",[194,1353,1354],{"class":124,"line":371},[194,1355,225],{"emptyLinePlaceholder":224},[194,1357,1358,1361,1363],{"class":124,"line":393},[194,1359,1360],{"class":203},"features, report ",[194,1362,234],{"class":199},[194,1364,1365],{"class":203}," [], []\n",[194,1367,1368,1370,1373,1375],{"class":124,"line":398},[194,1369,618],{"class":199},[194,1371,1372],{"class":203}," ev ",[194,1374,624],{"class":199},[194,1376,1377],{"class":203}," events.getFeatures():\n",[194,1379,1380,1382,1384,1387,1389],{"class":124,"line":409},[194,1381,663],{"class":203},[194,1383,234],{"class":199},[194,1385,1386],{"class":203}," route_lines.get(ev[",[194,1388,612],{"class":240},[194,1390,673],{"class":203},[194,1392,1393,1395,1397,1399,1401],{"class":124,"line":432},[194,1394,678],{"class":199},[194,1396,681],{"class":203},[194,1398,684],{"class":199},[194,1400,687],{"class":247},[194,1402,690],{"class":203},[194,1404,1405,1408,1410,1412,1414,1417,1420,1423,1425,1428],{"class":124,"line":443},[194,1406,1407],{"class":203},"        report.append(",[194,1409,839],{"class":199},[194,1411,267],{"class":240},[194,1413,844],{"class":247},[194,1415,1416],{"class":203},"ev[",[194,1418,1419],{"class":240},"'route_id'",[194,1421,1422],{"class":203},"]",[194,1424,853],{"class":247},[194,1426,1427],{"class":240},": no such route\"",[194,1429,330],{"class":203},[194,1431,1432],{"class":124,"line":802},[194,1433,695],{"class":199},[194,1435,1436,1438,1441,1443,1446,1448,1451,1453,1456],{"class":124,"line":831},[194,1437,678],{"class":199},[194,1439,1440],{"class":203}," ev[",[194,1442,1327],{"class":240},[194,1444,1445],{"class":203},"] ",[194,1447,819],{"class":199},[194,1449,1450],{"class":203}," line.length() ",[194,1452,454],{"class":199},[194,1454,1455],{"class":247}," 1",[194,1457,690],{"class":203},[194,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477,1480,1482,1485,1487,1489,1492,1494,1496,1499,1501,1503],{"class":124,"line":1460},21,[194,1462,1407],{"class":203},[194,1464,839],{"class":199},[194,1466,267],{"class":240},[194,1468,844],{"class":247},[194,1470,1416],{"class":203},[194,1472,1419],{"class":240},[194,1474,1422],{"class":203},[194,1476,853],{"class":247},[194,1478,1479],{"class":247}," {",[194,1481,1416],{"class":203},[194,1483,1484],{"class":240},"'start_m'",[194,1486,1422],{"class":203},[194,1488,853],{"class":247},[194,1490,1491],{"class":240},"–",[194,1493,844],{"class":247},[194,1495,1416],{"class":203},[194,1497,1498],{"class":240},"'end_m'",[194,1500,1422],{"class":203},[194,1502,853],{"class":247},[194,1504,1505],{"class":240},": \"\n",[194,1507,1509,1512,1515,1517,1520,1523,1525,1527],{"class":124,"line":1508},22,[194,1510,1511],{"class":199},"                      f",[194,1513,1514],{"class":240},"\"beyond length ",[194,1516,844],{"class":247},[194,1518,1519],{"class":203},"line.length()",[194,1521,1522],{"class":199},":.0f",[194,1524,853],{"class":247},[194,1526,267],{"class":240},[194,1528,330],{"class":203},[194,1530,1532,1535,1537],{"class":124,"line":1531},23,[194,1533,1534],{"class":203},"    f ",[194,1536,234],{"class":199},[194,1538,1539],{"class":203}," QgsFeature(out_fields)\n",[194,1541,1543,1546,1548,1551,1553],{"class":124,"line":1542},24,[194,1544,1545],{"class":203},"    f.setGeometry(section(line, ev[",[194,1547,1316],{"class":240},[194,1549,1550],{"class":203},"], ev[",[194,1552,1327],{"class":240},[194,1554,1555],{"class":203},"]))\n",[194,1557,1559,1562,1564,1566,1568,1570,1572,1574,1576],{"class":124,"line":1558},25,[194,1560,1561],{"class":203},"    f.setAttributes([ev[",[194,1563,612],{"class":240},[194,1565,1550],{"class":203},[194,1567,1316],{"class":240},[194,1569,1550],{"class":203},[194,1571,1327],{"class":240},[194,1573,1550],{"class":203},[194,1575,1333],{"class":240},[194,1577,1578],{"class":203},"]])\n",[194,1580,1582],{"class":124,"line":1581},26,[194,1583,1584],{"class":203},"    features.append(f)\n",[194,1586,1588],{"class":124,"line":1587},27,[194,1589,225],{"emptyLinePlaceholder":224},[194,1591,1593],{"class":124,"line":1592},28,[194,1594,1595],{"class":203},"out.dataProvider().addFeatures(features)\n",[194,1597,1599],{"class":124,"line":1598},29,[194,1600,1601],{"class":203},"QgsProject.instance().addMapLayer(out)\n",[194,1603,1605,1607,1609,1611,1614,1616,1619,1622,1625],{"class":124,"line":1604},30,[194,1606,303],{"class":247},[194,1608,306],{"class":203},[194,1610,267],{"class":240},[194,1612,1613],{"class":247},"\\n",[194,1615,267],{"class":240},[194,1617,1618],{"class":203},".join(report) ",[194,1620,1621],{"class":199},"or",[194,1623,1624],{"class":240}," \"all events within route lengths\"",[194,1626,330],{"class":203},[14,1628,1629,1631,1632,1635,1636,1639,1640,1644],{},[155,1630,477],{}," The one-metre tolerance on the length check avoids reporting rounding differences, while still catching events recorded against a different, longer version of the route. Clamped events are still created — the report is for review, not rejection — because a partly wrong section is usually more useful on the map than a missing one. The memory layer is the quick output; save it with ",[176,1633,1634],{},"native:savefeatures"," when the result is final. For a table that is already clean, ",[176,1637,1638],{},"native:linesubstring"," in Processing, or a virtual layer as in ",[26,1641,1643],{"href":1642},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fquery-layers-with-virtual-layer-sql-pyqgis\u002F","querying layers with virtual layer SQL",", are alternatives.",[144,1646,1648],{"id":1647},"direction-multipart-lines-and-evenly-spaced-points","Direction, multipart lines and evenly spaced points",[14,1650,1651],{},"A chainage is meaningless if the line runs the wrong way. Before trusting any result, confirm the first vertex is where chainage zero should be, and reverse lines that are backwards.",[185,1653,1655],{"className":187,"code":1654,"language":189,"meta":190,"style":190},"start_node = QgsGeometry.fromPointXY(QgsPointXY(430112.0, 431050.0))  # known 0 m marker\n\nfor rid, geom in route_lines.items():\n    first = QgsGeometry(geom.vertexAt(0))\n    if geom.isMultipart():\n        print(rid, \"is multipart:\", geom.constGet().numGeometries(), \"parts\")\n    if rid == \"A61\" and first.distance(start_node) > 50:\n        route_lines[rid] = QgsGeometry(geom.constGet().reversed())\n        print(rid, \"reversed so chainage starts at the known marker\")\n\nmarkers = processing.run(\"native:pointsalonglines\", {\n    \"INPUT\": routes, \"DISTANCE\": 100, \"START_OFFSET\": 0, \"END_OFFSET\": 0,\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\nprint(markers.featureCount(), \"100 m markers; 'distance' field holds each chainage\")\n",[176,1656,1657,1681,1685,1697,1711,1718,1737,1763,1773,1784,1788,1804,1842,1854,1864],{"__ignoreMap":190},[194,1658,1659,1662,1664,1667,1670,1672,1675,1678],{"class":124,"line":196},[194,1660,1661],{"class":203},"start_node ",[194,1663,234],{"class":199},[194,1665,1666],{"class":203}," QgsGeometry.fromPointXY(QgsPointXY(",[194,1668,1669],{"class":247},"430112.0",[194,1671,312],{"class":203},[194,1673,1674],{"class":247},"431050.0",[194,1676,1677],{"class":203},"))  ",[194,1679,1680],{"class":1245},"# known 0 m marker\n",[194,1682,1683],{"class":124,"line":207},[194,1684,225],{"emptyLinePlaceholder":224},[194,1686,1687,1689,1692,1694],{"class":124,"line":221},[194,1688,618],{"class":199},[194,1690,1691],{"class":203}," rid, geom ",[194,1693,624],{"class":199},[194,1695,1696],{"class":203}," route_lines.items():\n",[194,1698,1699,1702,1704,1707,1709],{"class":124,"line":228},[194,1700,1701],{"class":203},"    first ",[194,1703,234],{"class":199},[194,1705,1706],{"class":203}," QgsGeometry(geom.vertexAt(",[194,1708,51],{"class":247},[194,1710,281],{"class":203},[194,1712,1713,1715],{"class":124,"line":253},[194,1714,678],{"class":199},[194,1716,1717],{"class":203}," geom.isMultipart():\n",[194,1719,1720,1723,1726,1729,1732,1735],{"class":124,"line":284},[194,1721,1722],{"class":247},"        print",[194,1724,1725],{"class":203},"(rid, ",[194,1727,1728],{"class":240},"\"is multipart:\"",[194,1730,1731],{"class":203},", geom.constGet().numGeometries(), ",[194,1733,1734],{"class":240},"\"parts\"",[194,1736,330],{"class":203},[194,1738,1739,1741,1744,1747,1750,1753,1756,1758,1761],{"class":124,"line":295},[194,1740,678],{"class":199},[194,1742,1743],{"class":203}," rid ",[194,1745,1746],{"class":199},"==",[194,1748,1749],{"class":240}," \"A61\"",[194,1751,1752],{"class":199}," and",[194,1754,1755],{"class":203}," first.distance(start_node) ",[194,1757,819],{"class":199},[194,1759,1760],{"class":247}," 50",[194,1762,690],{"class":203},[194,1764,1765,1768,1770],{"class":124,"line":300},[194,1766,1767],{"class":203},"        route_lines[rid] ",[194,1769,234],{"class":199},[194,1771,1772],{"class":203}," QgsGeometry(geom.constGet().reversed())\n",[194,1774,1775,1777,1779,1782],{"class":124,"line":333},[194,1776,1722],{"class":247},[194,1778,1725],{"class":203},[194,1780,1781],{"class":240},"\"reversed so chainage starts at the known marker\"",[194,1783,330],{"class":203},[194,1785,1786],{"class":124,"line":338},[194,1787,225],{"emptyLinePlaceholder":224},[194,1789,1790,1793,1795,1798,1801],{"class":124,"line":349},[194,1791,1792],{"class":203},"markers ",[194,1794,234],{"class":199},[194,1796,1797],{"class":203}," processing.run(",[194,1799,1800],{"class":240},"\"native:pointsalonglines\"",[194,1802,1803],{"class":203},", {\n",[194,1805,1806,1809,1812,1815,1818,1821,1823,1826,1828,1830,1832,1835,1837,1839],{"class":124,"line":360},[194,1807,1808],{"class":240},"    \"INPUT\"",[194,1810,1811],{"class":203},": routes, ",[194,1813,1814],{"class":240},"\"DISTANCE\"",[194,1816,1817],{"class":203},": ",[194,1819,1820],{"class":247},"100",[194,1822,312],{"class":203},[194,1824,1825],{"class":240},"\"START_OFFSET\"",[194,1827,1817],{"class":203},[194,1829,51],{"class":247},[194,1831,312],{"class":203},[194,1833,1834],{"class":240},"\"END_OFFSET\"",[194,1836,1817],{"class":203},[194,1838,51],{"class":247},[194,1840,1841],{"class":203},",\n",[194,1843,1844,1847,1849,1852],{"class":124,"line":371},[194,1845,1846],{"class":240},"    \"OUTPUT\"",[194,1848,1817],{"class":203},[194,1850,1851],{"class":240},"\"TEMPORARY_OUTPUT\"",[194,1853,1841],{"class":203},[194,1855,1856,1859,1862],{"class":124,"line":393},[194,1857,1858],{"class":203},"})[",[194,1860,1861],{"class":240},"\"OUTPUT\"",[194,1863,250],{"class":203},[194,1865,1866,1868,1871,1874],{"class":124,"line":398},[194,1867,303],{"class":247},[194,1869,1870],{"class":203},"(markers.featureCount(), ",[194,1872,1873],{"class":240},"\"100 m markers; 'distance' field holds each chainage\"",[194,1875,330],{"class":203},[14,1877,1878,1880,1881,1884,1885,1888,1889,1892,1893,1897,1898,1901],{},[155,1879,477],{}," Comparing the first vertex against a known zero marker is the only reliable direction test, because direction is a convention of the organisation that owns the route, not a property of the geometry. ",[176,1882,1883],{},"reversed()"," flips a curve's vertex order without changing its shape. ",[176,1886,1887],{},"native:pointsalonglines"," generates a point every 100 m and writes the chainage of each into a ",[176,1890,1891],{},"distance"," field, which is a ready-made set of marker posts for labelling, and the way to sample a raster along a route as in ",[26,1894,1896],{"href":1895},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fextract-elevation-profile-along-line-pyqgis\u002F","extracting an elevation profile",". Remember to ",[176,1899,1900],{},"import processing"," first when running outside the Python console.",[144,1903,1905],{"id":1904},"qgis-version-compatibility","QGIS version compatibility",[14,1907,1908,312,1910,1913,1914,1916,1917,1919,1920,1922,1923,1926,1927,1930,1931,312,1934,1913,1937,1940,1941,312,1944,312,1947,1950],{},[176,1909,481],{},[176,1911,1912],{},"interpolateAngle"," and ",[176,1915,901],{}," have been part of ",[176,1918,1071],{}," since QGIS 3.0; ",[176,1921,1055],{}," since 3.4. ",[176,1924,1925],{},"QgsField"," with ",[176,1928,1929],{},"QMetaType.Type"," is the 3.38+ constructor and the only one on the QGIS 4 series; on older releases use ",[176,1932,1933],{},"QVariant.String",[176,1935,1936],{},"QVariant.Double",[176,1938,1939],{},"QVariant.Int",". Expression equivalents — ",[176,1942,1943],{},"line_interpolate_point",[176,1945,1946],{},"line_locate_point",[176,1948,1949],{},"line_substring"," — are available from 3.4 for use in field calculations and virtual fields.",[144,1952,1954],{"id":1953},"troubleshooting","Troubleshooting",[149,1956,1957,1965,1971,1977,1986],{},[152,1958,1959,1964],{},[155,1960,1961,1963],{},[176,1962,481],{}," returns an empty geometry."," The distance is beyond the line length or negative.",[152,1966,1967,1970],{},[155,1968,1969],{},"Chainages run backwards."," The line is digitised end to start; reverse it.",[152,1972,1973,1976],{},[155,1974,1975],{},"Results on the wrong road near junctions."," Points were measured against the nearest line instead of their own route.",[152,1978,1979,1982,1983,1985],{},[155,1980,1981],{},"Substrings are empty for multipart routes."," Merge parts with ",[176,1984,1079],{}," or explode and measure part by part with cumulative offsets.",[152,1987,1988,1991],{},[155,1989,1990],{},"Distances look like tiny decimals."," The layer is in degrees; reproject to a metric CRS first.",[144,1993,1995],{"id":1994},"conclusion","Conclusion",[14,1997,1998,1999,2001,2002,2004,2005,2007],{},"Keep routes single-part, projected in metres and digitised in chainage order, and check all three before trusting a result. Use ",[176,2000,481],{}," to place events, ",[176,2003,901],{}," with the right route and an offset check to measure them, and ",[176,2006,1055],{}," to cut sections from start and end distances — clamping and reporting rather than silently dropping the rows that do not fit.",[144,2009,2011],{"id":2010},"frequently-asked-questions","Frequently Asked Questions",[14,2013,2014,2017,2018,2021],{},[155,2015,2016],{},"Can I use M values instead of computing distances?","\nYes, if the lines carry calibrated measures. Read them per vertex with ",[176,2019,2020],{},"QgsPoint.m()"," and interpolate between vertices; this is the right approach when official chainages do not match geometric length.",[14,2023,2024,2027],{},[155,2025,2026],{},"How do I handle routes that are longer on the ground than on the map?","\nCalibrate: scale geometric distance by the ratio of official length to geometric length, or store M values from known marker posts.",[14,2029,2030,2033,2035],{},[155,2031,2032],{},"Is there a Processing algorithm for events tables?",[176,2034,1638],{}," cuts one distance range for every feature of a line layer; combine it with a join for per-row ranges, or use the loop above.",[14,2037,2038,2041,2042,1913,2044,2046],{},[155,2039,2040],{},"Does this work on curved geometries?","\nYes. ",[176,2043,1055],{},[176,2045,481],{}," work on compound curves and circular strings.",[144,2048,2050],{"id":2049},"related","Related",[149,2052,2053,2058,2064,2070,2076],{},[152,2054,2055,2057],{},[26,2056,29],{"href":28}," — the guide this recipe belongs to",[152,2059,2060],{},[26,2061,2063],{"href":2062},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fjoin-attributes-by-nearest-neighbour-pyqgis\u002F","Join Attributes by Nearest Neighbour in PyQGIS",[152,2065,2066],{},[26,2067,2069],{"href":2068},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fiterate-and-edit-geometry-vertices-pyqgis\u002F","Iterate and Edit Geometry Vertices in PyQGIS",[152,2071,2072],{},[26,2073,2075],{"href":2074},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fsnap-points-to-network-pyqgis\u002F","Snap Points to a Network in PyQGIS",[152,2077,2078],{},[26,2079,2080],{"href":1895},"Extract an Elevation Profile Along a Line in PyQGIS",[2082,2083,2084],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":190,"searchDepth":207,"depth":207,"links":2086},[2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097],{"id":146,"depth":207,"text":147},{"id":171,"depth":207,"text":172},{"id":489,"depth":207,"text":490},{"id":909,"depth":207,"text":910},{"id":1083,"depth":207,"text":1084},{"id":1647,"depth":207,"text":1648},{"id":1904,"depth":207,"text":1905},{"id":1953,"depth":207,"text":1954},{"id":1994,"depth":207,"text":1995},{"id":2010,"depth":207,"text":2011},{"id":2049,"depth":207,"text":2050},"Locate points by distance along a line and measure how far along a line a point lies — interpolate, lineLocatePoint, line substrings for sections between chainages, evenly spaced points and events tables, with the direction and CRS checks that keep chainages honest.","md",{"slug":2101,"type":2102,"breadcrumb":2103,"datePublished":2104,"dateModified":2104},"linear-referencing-along-lines-pyqgis","article","Linear Referencing Along Lines","2026-09-17","\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis",{"title":5,"description":2098},"spatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis\u002Findex","WqW1RKKLTHiDytKfy5NjZ4JcjNWzc-2MiU-vNOYxI8w",1789632908418]