[{"data":1,"prerenderedAt":1807},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing":3},{"id":4,"title":5,"body":6,"description":1796,"extension":1797,"meta":1798,"navigation":308,"path":1803,"seo":1804,"stem":1805,"__hash__":1806},"docs\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Findex.md","Network Analysis & Routing in PyQGIS",{"type":7,"value":8,"toc":1782},"minimark",[9,13,17,26,193,198,215,218,229,232,235,239,459,483,488,499,502,506,631,634,693,717,720,724,735,800,810,817,821,824,904,950,954,957,983,999,1005,1008,1012,1025,1158,1258,1281,1285,1288,1468,1493,1496,1500,1503,1506,1509,1615,1624,1632,1636,1669,1673,1679,1694,1700,1708,1718,1728,1732,1778],[10,11,5],"h1",{"id":12},"network-analysis-routing-in-pyqgis",[14,15,16],"p",{},"QGIS will route across any line layer you give it, and that is both the appeal and the trap. There is no road database behind the scenes, no turn restrictions, no traffic — just the geometry you supply and the rules you describe. Feed it a clean topological network and it produces answers that stand up. Feed it a cartographic road layer where the dual carriageways never quite touch, and it produces answers that look plausible and are wrong.",[14,18,19,20,25],{},"This guide sits inside ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation"," and covers the whole subject: how a line layer becomes a graph, how to describe direction and cost, how to run shortest paths and service areas from Python, and — the part that decides whether any of it works — how snapping and topology tolerance behave.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,103,109,113,116,120,125,129,132,136,142,146,149,153,160,164,168,171,174,182,186,189],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 356","img","The path from a line layer to a routing answer: a director describes direction, strategies describe cost, a builder produces a graph, and the analyzer runs Dijkstra over it","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Four objects between a line layer and a route",[40,41,42],"desc",{},"A line layer is handed to a director, which decides which way each edge may be travelled. One or more strategies attach a cost to each edge. A builder walks the layer and produces a graph of vertices and edges. The graph analyzer runs Dijkstra from a start vertex and returns a shortest-path tree, from which a route or a service area is read.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","356","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"narOvArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The graph is built once and answers many questions",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"20","54","150","76","#efeadd","#59645f","stroke-width:2.5",[68,86,90],{"x":87,"y":88,"style":89,"fill":73,"textAnchor":74},"95","82","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","line layer",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"106","text-anchor:middle;font-size:10.5px;font-family:sans-serif","roads · paths · pipes",[97,98],"line",{"x1":99,"y1":100,"x2":101,"y2":100,"stroke":66,"style":102},"170","92","192","stroke-width:2;marker-end:url(#narOvArrow)",[44,104],{"x":105,"y":79,"width":106,"height":81,"rx":58,"fill":107,"stroke":108,"style":84},"198","164","#eef7f4","#0f766e",[68,110,112],{"x":111,"y":88,"style":89,"fill":108,"textAnchor":74},"280","director",[68,114,115],{"x":111,"y":93,"style":94,"fill":66,"textAnchor":74},"which way may I go?",[97,117],{"x1":118,"y1":100,"x2":119,"y2":100,"stroke":66,"style":102},"362","384",[44,121],{"x":122,"y":79,"width":106,"height":81,"rx":58,"fill":123,"stroke":124,"style":84},"390","#eff3ff","#2563eb",[68,126,128],{"x":127,"y":88,"style":89,"fill":124,"textAnchor":74},"472","strategy",[68,130,131],{"x":127,"y":93,"style":94,"fill":66,"textAnchor":74},"what does it cost?",[97,133],{"x1":134,"y1":100,"x2":135,"y2":100,"stroke":66,"style":102},"554","576",[44,137],{"x":138,"y":79,"width":139,"height":81,"rx":58,"fill":140,"stroke":141,"style":84},"582","158","#fdf2e2","#b45309",[68,143,145],{"x":144,"y":88,"style":89,"fill":141,"textAnchor":74},"661","builder",[68,147,148],{"x":144,"y":93,"style":94,"fill":66,"textAnchor":74},"makes the graph",[63,150],{"d":151,"fill":152,"stroke":66,"style":102},"M661 130 L661 158 L380 158 L380 186","none",[44,154],{"x":155,"y":101,"width":156,"height":157,"rx":58,"fill":158,"stroke":159,"style":84},"266","228","72","#edf8e9","#15803d",[68,161,163],{"x":70,"y":162,"style":89,"fill":159,"textAnchor":74},"220","QgsGraphAnalyzer.dijkstra",[68,165,167],{"x":70,"y":166,"style":94,"fill":66,"textAnchor":74},"244","tree of predecessors + costs",[63,169],{"d":170,"fill":152,"stroke":66,"style":102},"M380 264 L380 288 L150 288 L150 306",[63,172],{"d":173,"fill":152,"stroke":66,"style":102},"M380 264 L380 288 L610 288 L610 306",[44,175],{"x":176,"y":177,"width":162,"height":178,"rx":179,"fill":180,"stroke":108,"style":181},"40","312","36","6","#fffdf7","stroke-width:2",[68,183,185],{"x":80,"y":184,"style":94,"fill":66,"textAnchor":74},"335","a route between two points",[44,187],{"x":188,"y":177,"width":162,"height":178,"rx":179,"fill":180,"stroke":108,"style":181},"500",[68,190,192],{"x":191,"y":184,"style":94,"fill":66,"textAnchor":74},"610","everywhere within a budget",[194,195,197],"h2",{"id":196},"what-the-routing-api-expects-from-your-data","What the routing API expects from your data",[14,199,200,201,205,206,210,211,214],{},"The single most important thing to understand about network analysis in QGIS is that it does not clean your data. ",[202,203,204],"code",{},"QgsGraphBuilder"," walks the line layer and creates a vertex at every line ",[207,208,209],"em",{},"endpoint",". Two lines are connected if — and only if — their endpoints coincide within the topology tolerance you set. Lines that cross without a shared vertex are not connected; a bridge and the road under it are correctly ",[207,212,213],{},"not"," connected, which is a feature, but so is a junction where the digitising was sloppy, which is not.",[14,216,217],{},"That has three consequences that shape every routing project:",[14,219,220,221,224,225,228],{},"Roads split at junctions are required. A single polyline running the length of a street with side roads meeting it in the middle produces a graph in which none of those side roads connect, because their endpoints land in the middle of the street's geometry rather than at its ends. Running ",[202,222,223],{},"native:splitwithlines"," on the layer against itself, or ",[202,226,227],{},"native:explodelines",", fixes it.",[14,230,231],{},"Direction is data, not geometry. QGIS reads which way an edge may be travelled from an attribute you nominate, comparing its value against three strings you supply for forward, backward and both. There is no convention it assumes.",[14,233,234],{},"Costs come from strategies. Distance is free — QGIS computes it from the geometry. Anything else, most usefully travel time, comes from a speed attribute you provide.",[194,236,238],{"id":237},"building-the-graph","Building the graph",[240,241,246],"pre",{"className":242,"code":243,"language":244,"meta":245,"style":245},"language-python shiki shiki-themes github-dark","from qgis.analysis import (\n    QgsVectorLayerDirector,\n    QgsNetworkDistanceStrategy,\n    QgsGraphBuilder,\n)\nfrom qgis.core import QgsProject\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\ndirector = QgsVectorLayerDirector(\n    roads,\n    -1,      # direction field index; -1 means \"no field, use the default\"\n    \"\",      # value meaning forward only\n    \"\",      # value meaning backward only\n    \"\",      # value meaning both\n    QgsVectorLayerDirector.DirectionBoth,\n)\ndirector.addStrategy(QgsNetworkDistanceStrategy())\n\nbuilder = QgsGraphBuilder(roads.sourceCrs())\ntied = director.makeGraph(builder, [start_point, end_point])\ngraph = builder.graph()\n","python","",[202,247,248,266,272,278,284,290,303,310,335,340,351,357,373,384,394,404,410,415,421,426,437,448],{"__ignoreMap":245},[249,250,252,256,260,263],"span",{"class":97,"line":251},1,[249,253,255],{"class":254},"snl16","from",[249,257,259],{"class":258},"s95oV"," qgis.analysis ",[249,261,262],{"class":254},"import",[249,264,265],{"class":258}," (\n",[249,267,269],{"class":97,"line":268},2,[249,270,271],{"class":258},"    QgsVectorLayerDirector,\n",[249,273,275],{"class":97,"line":274},3,[249,276,277],{"class":258},"    QgsNetworkDistanceStrategy,\n",[249,279,281],{"class":97,"line":280},4,[249,282,283],{"class":258},"    QgsGraphBuilder,\n",[249,285,287],{"class":97,"line":286},5,[249,288,289],{"class":258},")\n",[249,291,293,295,298,300],{"class":97,"line":292},6,[249,294,255],{"class":254},[249,296,297],{"class":258}," qgis.core ",[249,299,262],{"class":254},[249,301,302],{"class":258}," QgsProject\n",[249,304,306],{"class":97,"line":305},7,[249,307,309],{"emptyLinePlaceholder":308},true,"\n",[249,311,313,316,319,322,326,329,332],{"class":97,"line":312},8,[249,314,315],{"class":258},"roads ",[249,317,318],{"class":254},"=",[249,320,321],{"class":258}," QgsProject.instance().mapLayersByName(",[249,323,325],{"class":324},"sU2Wk","\"roads\"",[249,327,328],{"class":258},")[",[249,330,46],{"class":331},"sDLfK",[249,333,334],{"class":258},"]\n",[249,336,338],{"class":97,"line":337},9,[249,339,309],{"emptyLinePlaceholder":308},[249,341,343,346,348],{"class":97,"line":342},10,[249,344,345],{"class":258},"director ",[249,347,318],{"class":254},[249,349,350],{"class":258}," QgsVectorLayerDirector(\n",[249,352,354],{"class":97,"line":353},11,[249,355,356],{"class":258},"    roads,\n",[249,358,360,363,366,369],{"class":97,"line":359},12,[249,361,362],{"class":254},"    -",[249,364,365],{"class":331},"1",[249,367,368],{"class":258},",      ",[249,370,372],{"class":371},"sjoCn","# direction field index; -1 means \"no field, use the default\"\n",[249,374,376,379,381],{"class":97,"line":375},13,[249,377,378],{"class":324},"    \"\"",[249,380,368],{"class":258},[249,382,383],{"class":371},"# value meaning forward only\n",[249,385,387,389,391],{"class":97,"line":386},14,[249,388,378],{"class":324},[249,390,368],{"class":258},[249,392,393],{"class":371},"# value meaning backward only\n",[249,395,397,399,401],{"class":97,"line":396},15,[249,398,378],{"class":324},[249,400,368],{"class":258},[249,402,403],{"class":371},"# value meaning both\n",[249,405,407],{"class":97,"line":406},16,[249,408,409],{"class":258},"    QgsVectorLayerDirector.DirectionBoth,\n",[249,411,413],{"class":97,"line":412},17,[249,414,289],{"class":258},[249,416,418],{"class":97,"line":417},18,[249,419,420],{"class":258},"director.addStrategy(QgsNetworkDistanceStrategy())\n",[249,422,424],{"class":97,"line":423},19,[249,425,309],{"emptyLinePlaceholder":308},[249,427,429,432,434],{"class":97,"line":428},20,[249,430,431],{"class":258},"builder ",[249,433,318],{"class":254},[249,435,436],{"class":258}," QgsGraphBuilder(roads.sourceCrs())\n",[249,438,440,443,445],{"class":97,"line":439},21,[249,441,442],{"class":258},"tied ",[249,444,318],{"class":254},[249,446,447],{"class":258}," director.makeGraph(builder, [start_point, end_point])\n",[249,449,451,454,456],{"class":97,"line":450},22,[249,452,453],{"class":258},"graph ",[249,455,318],{"class":254},[249,457,458],{"class":258}," builder.graph()\n",[14,460,461,465,466,469,470,473,474,477,478,482],{},[462,463,464],"strong",{},"Breakdown:"," The six director arguments are positional and easy to get wrong; passing ",[202,467,468],{},"-1"," for the field index makes the three value strings irrelevant and treats every edge as bidirectional, which is the right starting point while you are still checking that the topology works at all. ",[202,471,472],{},"makeGraph()"," does two things at once: it builds the graph, and it ",[207,475,476],{},"snaps"," the points you pass onto the nearest edge, returning the snapped positions in the same order. Those returned points, not your originals, are what you look up in the graph afterwards — a distinction covered in ",[21,479,481],{"href":480},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fsnap-points-to-network-pyqgis\u002F","snapping points to a network",".",[14,484,485,487],{},[202,486,204],{}," takes an optional topology tolerance as its third argument. Left at zero, endpoints must coincide exactly; set to 0.5 in a metric CRS, endpoints within half a metre are merged into one vertex. That single number rescues most real-world road layers and, set too high, silently welds together roads that should not meet.",[14,489,490,491,494,495,498],{},"Two properties of the returned graph are worth knowing before you use it. It is ",[207,492,493],{},"directed",": a two-way street becomes two edges, one each way, so the edge count is roughly twice the segment count and a graph over a 200,000-segment road layer holds around 400,000 edges. And it is ",[207,496,497],{},"detached from the source layer",": once built, it holds no reference to features, so you cannot ask an edge which road it came from. If you need that link — to report street names along a route, say — record it yourself by keeping a parallel list as you walk the layer, or accept that you will re-intersect the output geometry with the source layer afterwards.",[14,500,501],{},"The build itself is the expensive step. On a moderate city network it takes a few seconds; on a national road layer it takes minutes and a great deal of memory. That cost profile is the reason the graph API and the Processing algorithms occupy different niches, which the comparison further down makes concrete.",[194,503,505],{"id":504},"direction-one-way-streets-and-turn-penalties","Direction, one-way streets and turn penalties",[14,507,508],{},[29,509,512,515,518,521,528,532,537,542,546,549,552,555,560,566,571,576,579,583,586,589,591,595,600,603,607,611,614,618,622,626],{"viewBox":510,"role":32,"ariaLabel":511,"xmlns":34},"0 0 760 328","How a direction field is interpreted: each edge's attribute value is compared against the forward, backward and both strings, and anything unmatched falls back to the default direction",[36,513,514],{},"The direction field is a string comparison",[40,516,517],{},"For every edge, the director reads the nominated attribute and compares it against three strings supplied at construction. A match on the forward string allows travel only along the digitised direction; a match on the backward string allows only the reverse; a match on the both string allows either. Any other value, including null, falls through to the default direction, which is why a typo in the data silently produces a two-way street.",[44,519],{"x":46,"y":46,"width":47,"height":520,"fill":49},"328",[51,522,523],{},[54,524,526],{"id":525,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"narDirArrow",[63,527],{"d":65,"fill":66},[68,529,531],{"x":70,"y":530,"style":72,"fill":73,"textAnchor":74},"26","Four outcomes, and the fourth is the dangerous one",[44,533],{"x":534,"y":535,"width":162,"height":536,"rx":58,"fill":82,"stroke":83,"style":84},"270","44","46",[68,538,541],{"x":70,"y":539,"style":540,"fill":73,"textAnchor":74},"73","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","oneway attribute value",[63,543],{"d":544,"fill":152,"stroke":66,"style":545},"M380 90 L380 110 L110 110 L110 136","stroke-width:2;marker-end:url(#narDirArrow)",[63,547],{"d":548,"fill":152,"stroke":66,"style":545},"M380 90 L380 110 L290 110 L290 136",[63,550],{"d":551,"fill":152,"stroke":66,"style":545},"M380 90 L380 110 L470 110 L470 136",[63,553],{"d":554,"fill":152,"stroke":66,"style":545},"M380 90 L380 110 L650 110 L650 136",[44,556],{"x":557,"y":558,"width":559,"height":93,"rx":58,"fill":107,"stroke":108,"style":84},"24","142","172",[68,561,565],{"x":562,"y":563,"style":564,"fill":108,"textAnchor":74},"110","168","text-anchor:middle;font-size:11px;font-weight:bold;font-family:monospace","\"F\"",[97,567],{"x1":568,"y1":569,"x2":559,"y2":569,"stroke":108,"style":570},"48","196","stroke-width:2.6;marker-end:url(#narDirArrow)",[68,572,575],{"x":562,"y":573,"style":574,"fill":66,"textAnchor":74},"226","text-anchor:middle;font-size:10px;font-family:sans-serif","digitised direction only",[44,577],{"x":578,"y":558,"width":559,"height":93,"rx":58,"fill":123,"stroke":124,"style":84},"204",[68,580,582],{"x":581,"y":563,"style":564,"fill":124,"textAnchor":74},"290","\"T\"",[97,584],{"x1":585,"y1":569,"x2":156,"y2":569,"stroke":124,"style":570},"352",[68,587,588],{"x":581,"y":573,"style":574,"fill":66,"textAnchor":74},"reverse only",[44,590],{"x":119,"y":558,"width":559,"height":93,"rx":58,"fill":158,"stroke":159,"style":84},[68,592,594],{"x":593,"y":563,"style":564,"fill":159,"textAnchor":74},"470","\"B\"",[97,596],{"x1":597,"y1":569,"x2":598,"y2":569,"stroke":159,"style":599},"408","532","stroke-width:2.6;marker-end:url(#narDirArrow);marker-start:url(#narDirArrow)",[68,601,602],{"x":593,"y":573,"style":574,"fill":66,"textAnchor":74},"both ways",[44,604],{"x":605,"y":558,"width":559,"height":93,"rx":58,"fill":180,"stroke":606,"style":84},"564","#b91c1c",[68,608,610],{"x":609,"y":563,"style":564,"fill":606,"textAnchor":74},"650","NULL · \"yes\" · \"1\"",[68,612,613],{"x":609,"y":569,"style":94,"fill":66,"textAnchor":74},"falls through to",[68,615,617],{"x":609,"y":616,"style":94,"fill":66,"textAnchor":74},"216","the default direction",[68,619,621],{"x":609,"y":620,"style":574,"fill":606,"textAnchor":74},"238","no warning is issued",[44,623],{"x":624,"y":534,"width":625,"height":536,"rx":58,"fill":140,"stroke":141,"style":181},"80","600",[68,627,630],{"x":70,"y":628,"style":629,"fill":66,"textAnchor":74},"298","text-anchor:middle;font-size:11px;font-family:sans-serif","count the distinct values in the field before you trust the routing",[14,632,633],{},"Wiring a real one-way field is the same constructor with the strings filled in:",[240,635,637],{"className":242,"code":636,"language":244,"meta":245,"style":245},"field_index = roads.fields().indexOf(\"oneway\")\n\ndirector = QgsVectorLayerDirector(\n    roads, field_index, \"F\", \"T\", \"B\",\n    QgsVectorLayerDirector.DirectionBoth,\n)\n",[202,638,639,654,658,666,685,689],{"__ignoreMap":245},[249,640,641,644,646,649,652],{"class":97,"line":251},[249,642,643],{"class":258},"field_index ",[249,645,318],{"class":254},[249,647,648],{"class":258}," roads.fields().indexOf(",[249,650,651],{"class":324},"\"oneway\"",[249,653,289],{"class":258},[249,655,656],{"class":97,"line":268},[249,657,309],{"emptyLinePlaceholder":308},[249,659,660,662,664],{"class":97,"line":274},[249,661,345],{"class":258},[249,663,318],{"class":254},[249,665,350],{"class":258},[249,667,668,671,673,676,678,680,682],{"class":97,"line":280},[249,669,670],{"class":258},"    roads, field_index, ",[249,672,565],{"class":324},[249,674,675],{"class":258},", ",[249,677,582],{"class":324},[249,679,675],{"class":258},[249,681,594],{"class":324},[249,683,684],{"class":258},",\n",[249,686,687],{"class":97,"line":286},[249,688,409],{"class":258},[249,690,691],{"class":97,"line":292},[249,692,289],{"class":258},[14,694,695,697,698,700,701,704,705,708,709,712,713,716],{},[462,696,464],{}," The comparison is exact and case-sensitive against the attribute rendered as a string, so ",[202,699,565],{}," will not match ",[202,702,703],{},"\"f\""," or ",[202,706,707],{},"\" F\"",". The default direction is applied to everything that matches none of the three — including nulls, which in most road datasets are the overwhelming majority, since only the one-way streets carry a value. Choosing ",[202,710,711],{},"DirectionBoth"," as the default is therefore usually right, and choosing ",[202,714,715],{},"DirectionForward"," by mistake makes every unmarked street one-way, producing routes that wander absurdly.",[14,718,719],{},"What QGIS does not model is turn restrictions. No-left-turns, banned manoeuvres and turn penalties have no representation in this API. Where they matter — urban delivery routing, in particular — the honest answer is that QGIS's network analysis is the wrong tool and a dedicated routing engine is the right one.",[194,721,723],{"id":722},"cost-strategies-distance-and-time","Cost strategies: distance and time",[14,725,726,727,730,731,734],{},"A strategy attaches a number to each edge, and the analyzer minimises the sum along the path. ",[202,728,729],{},"QgsNetworkDistanceStrategy"," uses geometric length in the layer's units. ",[202,732,733],{},"QgsNetworkSpeedStrategy"," divides that length by a speed read from an attribute, producing a travel time.",[240,736,738],{"className":242,"code":737,"language":244,"meta":245,"style":245},"from qgis.analysis import QgsNetworkSpeedStrategy\n\nspeed_index = roads.fields().indexOf(\"speed_kph\")\nstrategy = QgsNetworkSpeedStrategy(speed_index, 50.0, 1000.0 \u002F 3600.0)\ndirector.addStrategy(strategy)\n",[202,739,740,751,755,769,795],{"__ignoreMap":245},[249,741,742,744,746,748],{"class":97,"line":251},[249,743,255],{"class":254},[249,745,259],{"class":258},[249,747,262],{"class":254},[249,749,750],{"class":258}," QgsNetworkSpeedStrategy\n",[249,752,753],{"class":97,"line":268},[249,754,309],{"emptyLinePlaceholder":308},[249,756,757,760,762,764,767],{"class":97,"line":274},[249,758,759],{"class":258},"speed_index ",[249,761,318],{"class":254},[249,763,648],{"class":258},[249,765,766],{"class":324},"\"speed_kph\"",[249,768,289],{"class":258},[249,770,771,774,776,779,782,784,787,790,793],{"class":97,"line":280},[249,772,773],{"class":258},"strategy ",[249,775,318],{"class":254},[249,777,778],{"class":258}," QgsNetworkSpeedStrategy(speed_index, ",[249,780,781],{"class":331},"50.0",[249,783,675],{"class":258},[249,785,786],{"class":331},"1000.0",[249,788,789],{"class":254}," \u002F",[249,791,792],{"class":331}," 3600.0",[249,794,289],{"class":258},[249,796,797],{"class":97,"line":286},[249,798,799],{"class":258},"director.addStrategy(strategy)\n",[14,801,802,804,805,809],{},[462,803,464],{}," The three arguments are the field index, the default speed used where the field is null, and a multiplier converting the field's units into layer-units-per-second. For a layer in metres with speeds in km\u002Fh the multiplier is 1000\u002F3600; get it wrong and every travel time is out by a constant factor, which is invisible in a route and fatal in a service area. Adding several strategies is allowed — each becomes a numbered criterion, and the analyzer takes an index to say which one to minimise, which is how one graph answers both \"shortest\" and \"fastest\". ",[21,806,808],{"href":807},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fadd-speed-and-travel-cost-to-network-pyqgis\u002F","Adding speed and travel cost to a network"," works through the unit arithmetic in detail.",[14,811,812,813,816],{},"A subtlety that catches people out: adding two strategies does not give you a graph that can be queried on either criterion independently in every sense. Each edge carries a cost ",[207,814,815],{},"vector",", and the analyzer's criterion index picks which element to minimise, so the routing is genuinely independent per criterion. But the strategies are evaluated when the graph is built, which means changing a speed assumption requires rebuilding. Where you want to compare several speed scenarios, add several speed strategies up front — one per scenario — and switch criterion index at query time. That builds once and answers all of them.",[194,818,820],{"id":819},"running-the-analysis","Running the analysis",[14,822,823],{},"With a graph in hand, Dijkstra does the work:",[240,825,827],{"className":242,"code":826,"language":244,"meta":245,"style":245},"from qgis.analysis import QgsGraphAnalyzer\n\nstart_vertex = graph.findVertex(tied[0])\ntree, costs = QgsGraphAnalyzer.dijkstra(graph, start_vertex, 0)\n\nend_vertex = graph.findVertex(tied[1])\nprint(\"cost to destination:\", costs[end_vertex])\n",[202,828,829,840,844,859,873,877,890],{"__ignoreMap":245},[249,830,831,833,835,837],{"class":97,"line":251},[249,832,255],{"class":254},[249,834,259],{"class":258},[249,836,262],{"class":254},[249,838,839],{"class":258}," QgsGraphAnalyzer\n",[249,841,842],{"class":97,"line":268},[249,843,309],{"emptyLinePlaceholder":308},[249,845,846,849,851,854,856],{"class":97,"line":274},[249,847,848],{"class":258},"start_vertex ",[249,850,318],{"class":254},[249,852,853],{"class":258}," graph.findVertex(tied[",[249,855,46],{"class":331},[249,857,858],{"class":258},"])\n",[249,860,861,864,866,869,871],{"class":97,"line":280},[249,862,863],{"class":258},"tree, costs ",[249,865,318],{"class":254},[249,867,868],{"class":258}," QgsGraphAnalyzer.dijkstra(graph, start_vertex, ",[249,870,46],{"class":331},[249,872,289],{"class":258},[249,874,875],{"class":97,"line":286},[249,876,309],{"emptyLinePlaceholder":308},[249,878,879,882,884,886,888],{"class":97,"line":292},[249,880,881],{"class":258},"end_vertex ",[249,883,318],{"class":254},[249,885,853],{"class":258},[249,887,365],{"class":331},[249,889,858],{"class":258},[249,891,892,895,898,901],{"class":97,"line":305},[249,893,894],{"class":331},"print",[249,896,897],{"class":258},"(",[249,899,900],{"class":324},"\"cost to destination:\"",[249,902,903],{"class":258},", costs[end_vertex])\n",[14,905,906,908,909,912,913,916,917,920,921,924,925,927,928,931,932,935,936,939,940,944,945,949],{},[462,907,464],{}," ",[202,910,911],{},"dijkstra"," returns two parallel lists indexed by vertex id. ",[202,914,915],{},"tree[v]"," is the id of the ",[207,918,919],{},"edge"," used to arrive at vertex ",[202,922,923],{},"v",", or ",[202,926,468],{}," where the vertex was never reached; ",[202,929,930],{},"costs[v]"," is the accumulated cost, and it is a very large number rather than infinity for unreachable vertices. The third argument selects which strategy to minimise, in the order they were added. Walking backwards through ",[202,933,934],{},"tree"," from the destination reconstructs the route, and reading ",[202,937,938],{},"costs"," against a budget gives you a service area — the same call answers both, which is why ",[21,941,943],{"href":942},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fshortest-path-between-points-pyqgis\u002F","shortest paths"," and ",[21,946,948],{"href":947},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fcalculate-service-areas-pyqgis\u002F","service areas"," sit next to each other.",[194,951,953],{"id":952},"where-the-network-comes-from","Where the network comes from",[14,955,956],{},"Three sources cover most projects, and each brings its own preparation problem.",[14,958,959,962,963,966,967,675,970,675,973,975,976,978,979,982],{},[462,960,961],{},"OpenStreetMap"," extracts are the usual starting point for road routing. They come pre-noded at junctions, which removes the biggest data problem before it starts, and they carry a ",[202,964,965],{},"oneway"," tag whose values are ",[202,968,969],{},"yes",[202,971,972],{},"no",[202,974,468],{}," and a scattering of others. Those are strings, so they map directly onto the director's three values — with the important caveat that ",[202,977,468],{}," means \"one way against the digitised direction\", which is the ",[207,980,981],{},"backward"," value and not a missing value.",[14,984,985,988,989,675,992,675,995,998],{},[462,986,987],{},"National mapping agency road centrelines"," are usually topologically clean and carry a direction attribute under some local name and coding. They are also usually licensed, which matters if the output is published. Their weakness is that the direction coding is often numeric (1, 2, 3) and the director compares strings, so the values you pass must be ",[202,990,991],{},"\"1\"",[202,993,994],{},"\"2\"",[202,996,997],{},"\"3\""," rather than integers.",[14,1000,1001,1004],{},[462,1002,1003],{},"Your own digitised network"," — a site's paths, a utility's pipes, a warehouse's aisles — is where the topology problems live, because it was drawn for a map rather than for a graph. Expect to split lines at intersections, expect a tolerance in the tens of centimetres, and expect to find a handful of genuinely disconnected fragments that need editing rather than a tolerance increase.",[14,1006,1007],{},"Whichever the source, the routing API cares only about geometry and one or two attributes, so the preparation is always the same shape: get it into a metric CRS, node it, and check what fraction of it is connected.",[194,1009,1011],{"id":1010},"the-processing-route","The Processing route",[14,1013,1014,1015,675,1018,675,1021,1024],{},"Everything above also exists as Processing algorithms — ",[202,1016,1017],{},"native:shortestpathpointtopoint",[202,1019,1020],{},"native:shortestpathpointtolayer",[202,1022,1023],{},"native:serviceareafrompoint"," and their siblings. They wrap the same classes with the same parameters, and for a one-off answer they are simply less code.",[14,1026,1027],{},[29,1028,1031,1034,1037,1040,1047,1050,1055,1058,1065,1069,1074,1076,1080,1083,1086,1089,1091,1093,1096,1100,1103,1106,1111,1115,1119,1124,1127,1132,1135,1137,1140,1142,1145,1149,1154],{"viewBox":1029,"role":32,"ariaLabel":1030,"xmlns":34},"0 0 760 300","A comparison of the Processing algorithms and the graph API: algorithms rebuild the graph on every call, while the API builds once and reuses it across many queries",[36,1032,1033],{},"One question or ten thousand",[40,1035,1036],{},"A Processing algorithm builds the graph, answers one question and discards it, which is ideal for a single route. The graph API builds the graph once and answers any number of questions against it, which is the only workable approach for a matrix of thousands of origin-destination pairs.",[44,1038],{"x":46,"y":46,"width":47,"height":1039,"fill":49},"300",[51,1041,1042],{},[54,1043,1045],{"id":1044,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"narCmpArrow",[63,1046],{"d":65,"fill":66},[68,1048,1049],{"x":70,"y":530,"style":72,"fill":73,"textAnchor":74},"Build cost is paid per call, or once",[44,1051],{"x":557,"y":536,"width":1052,"height":1053,"rx":1054,"fill":107,"stroke":108,"style":84},"344","212","10",[68,1056,1057],{"x":569,"y":157,"style":89,"fill":108,"textAnchor":74},"Processing algorithm",[44,1059],{"x":568,"y":1060,"width":1061,"height":1062,"rx":179,"fill":1063,"stroke":159,"style":1064},"90","128","34","#e8efe6","stroke-width:1.8",[68,1066,1068],{"x":1067,"y":1067,"style":574,"fill":66,"textAnchor":74},"112","build graph",[97,1070],{"x1":1071,"y1":1072,"x2":105,"y2":1072,"stroke":66,"style":1073},"176","107","stroke-width:1.8;marker-end:url(#narCmpArrow)",[44,1075],{"x":578,"y":1060,"width":1061,"height":1062,"rx":179,"fill":1063,"stroke":159,"style":1064},[68,1077,1079],{"x":1078,"y":1067,"style":574,"fill":66,"textAnchor":74},"268","one answer",[44,1081],{"x":568,"y":1082,"width":1061,"height":1062,"rx":179,"fill":1063,"stroke":159,"style":1064},"140",[68,1084,1068],{"x":1067,"y":1085,"style":574,"fill":66,"textAnchor":74},"162",[97,1087],{"x1":1071,"y1":1088,"x2":105,"y2":1088,"stroke":66,"style":1073},"157",[44,1090],{"x":578,"y":1082,"width":1061,"height":1062,"rx":179,"fill":1063,"stroke":159,"style":1064},[68,1092,1079],{"x":1078,"y":1085,"style":574,"fill":66,"textAnchor":74},[68,1094,1095],{"x":569,"y":578,"style":94,"fill":66,"textAnchor":74},"two calls, two builds",[68,1097,1099],{"x":569,"y":1098,"style":574,"fill":83,"textAnchor":74},"230","perfect for one route, hopeless for a matrix",[44,1101],{"x":1102,"y":536,"width":1052,"height":1053,"rx":1054,"fill":123,"stroke":124,"style":84},"392",[68,1104,1105],{"x":605,"y":157,"style":89,"fill":124,"textAnchor":74},"graph API",[44,1107],{"x":1108,"y":1060,"width":1061,"height":1109,"rx":179,"fill":1110,"stroke":124,"style":1064},"416","84","#dbeafe",[68,1112,1068],{"x":1113,"y":1114,"style":574,"fill":66,"textAnchor":74},"480","126",[68,1116,1118],{"x":1113,"y":1117,"style":574,"fill":66,"textAnchor":74},"146","once",[97,1120],{"x1":1121,"y1":1122,"x2":1123,"y2":1122,"stroke":66,"style":1073},"544","132","566",[44,1125],{"x":1126,"y":1060,"width":1082,"height":530,"rx":179,"fill":1110,"stroke":124,"style":1064},"572",[68,1128,1131],{"x":1129,"y":1130,"style":574,"fill":66,"textAnchor":74},"642","108","answer",[44,1133],{"x":1126,"y":1134,"width":1082,"height":530,"rx":179,"fill":1110,"stroke":124,"style":1064},"122",[68,1136,1131],{"x":1129,"y":1082,"style":574,"fill":66,"textAnchor":74},[44,1138],{"x":1126,"y":1139,"width":1082,"height":530,"rx":179,"fill":1110,"stroke":124,"style":1064},"154",[68,1141,1131],{"x":1129,"y":559,"style":574,"fill":66,"textAnchor":74},[68,1143,1144],{"x":605,"y":1053,"style":94,"fill":66,"textAnchor":74},"one build, any number of queries",[68,1146,1148],{"x":605,"y":1147,"style":574,"fill":83,"textAnchor":74},"234","the only way to do an origin-destination matrix",[44,1150],{"x":1151,"y":1152,"width":1153,"height":557,"rx":179,"fill":140,"stroke":141,"style":1064},"180","272","400",[68,1155,1157],{"x":70,"y":1156,"style":94,"fill":66,"textAnchor":74},"289","graph build is the expensive part on a large network",[240,1159,1161],{"className":242,"code":1160,"language":244,"meta":245,"style":245},"import processing\n\nprocessing.run(\"native:shortestpathpointtopoint\", {\n    \"INPUT\": roads,\n    \"STRATEGY\": 0,\n    \"START_POINT\": \"445120,187330 [EPSG:27700]\",\n    \"END_POINT\": \"447980,189010 [EPSG:27700]\",\n    \"TOLERANCE\": 0.5,\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})\n",[202,1162,1163,1170,1174,1185,1193,1205,1217,1229,1241,1253],{"__ignoreMap":245},[249,1164,1165,1167],{"class":97,"line":251},[249,1166,262],{"class":254},[249,1168,1169],{"class":258}," processing\n",[249,1171,1172],{"class":97,"line":268},[249,1173,309],{"emptyLinePlaceholder":308},[249,1175,1176,1179,1182],{"class":97,"line":274},[249,1177,1178],{"class":258},"processing.run(",[249,1180,1181],{"class":324},"\"native:shortestpathpointtopoint\"",[249,1183,1184],{"class":258},", {\n",[249,1186,1187,1190],{"class":97,"line":280},[249,1188,1189],{"class":324},"    \"INPUT\"",[249,1191,1192],{"class":258},": roads,\n",[249,1194,1195,1198,1201,1203],{"class":97,"line":286},[249,1196,1197],{"class":324},"    \"STRATEGY\"",[249,1199,1200],{"class":258},": ",[249,1202,46],{"class":331},[249,1204,684],{"class":258},[249,1206,1207,1210,1212,1215],{"class":97,"line":292},[249,1208,1209],{"class":324},"    \"START_POINT\"",[249,1211,1200],{"class":258},[249,1213,1214],{"class":324},"\"445120,187330 [EPSG:27700]\"",[249,1216,684],{"class":258},[249,1218,1219,1222,1224,1227],{"class":97,"line":305},[249,1220,1221],{"class":324},"    \"END_POINT\"",[249,1223,1200],{"class":258},[249,1225,1226],{"class":324},"\"447980,189010 [EPSG:27700]\"",[249,1228,684],{"class":258},[249,1230,1231,1234,1236,1239],{"class":97,"line":312},[249,1232,1233],{"class":324},"    \"TOLERANCE\"",[249,1235,1200],{"class":258},[249,1237,1238],{"class":331},"0.5",[249,1240,684],{"class":258},[249,1242,1243,1246,1248,1251],{"class":97,"line":337},[249,1244,1245],{"class":324},"    \"OUTPUT\"",[249,1247,1200],{"class":258},[249,1249,1250],{"class":324},"\"TEMPORARY_OUTPUT\"",[249,1252,684],{"class":258},[249,1254,1255],{"class":97,"line":342},[249,1256,1257],{"class":258},"})\n",[14,1259,1260,908,1262,1265,1266,704,1269,1272,1273,1276,1277,1280],{},[462,1261,464],{},[202,1263,1264],{},"STRATEGY"," is 0 for shortest and 1 for fastest; the fastest variant needs ",[202,1267,1268],{},"SPEED_FIELD",[202,1270,1271],{},"DEFAULT_SPEED"," to mean anything. Points are strings of ",[202,1274,1275],{},"x,y [authid]",", which is the Processing point-parameter convention rather than a geometry object. ",[202,1278,1279],{},"TOLERANCE"," is the same topology tolerance the builder takes, and it is worth setting explicitly rather than accepting the default on any real road layer.",[194,1282,1284],{"id":1283},"reconstructing-the-route-geometry","Reconstructing the route geometry",[14,1286,1287],{},"The predecessor tree is a list of integers; turning it into a line takes a short backward walk.",[240,1289,1291],{"className":242,"code":1290,"language":244,"meta":245,"style":245},"from qgis.core import QgsGeometry\n\nroute = [graph.vertex(end_vertex).point()]\ncurrent = end_vertex\n\nwhile current != start_vertex:\n    edge_id = tree[current]\n    if edge_id == -1:\n        raise SystemExit(\"destination is not reachable from the start\")\n    current = graph.edge(edge_id).fromVertex()\n    route.insert(0, graph.vertex(current).point())\n\ngeometry = QgsGeometry.fromPolylineXY(route)\nprint(f\"{geometry.length():.0f} m along {len(route) - 1} edges\")\n",[202,1292,1293,1304,1308,1318,1328,1332,1346,1356,1375,1390,1400,1410,1414,1424],{"__ignoreMap":245},[249,1294,1295,1297,1299,1301],{"class":97,"line":251},[249,1296,255],{"class":254},[249,1298,297],{"class":258},[249,1300,262],{"class":254},[249,1302,1303],{"class":258}," QgsGeometry\n",[249,1305,1306],{"class":97,"line":268},[249,1307,309],{"emptyLinePlaceholder":308},[249,1309,1310,1313,1315],{"class":97,"line":274},[249,1311,1312],{"class":258},"route ",[249,1314,318],{"class":254},[249,1316,1317],{"class":258}," [graph.vertex(end_vertex).point()]\n",[249,1319,1320,1323,1325],{"class":97,"line":280},[249,1321,1322],{"class":258},"current ",[249,1324,318],{"class":254},[249,1326,1327],{"class":258}," end_vertex\n",[249,1329,1330],{"class":97,"line":286},[249,1331,309],{"emptyLinePlaceholder":308},[249,1333,1334,1337,1340,1343],{"class":97,"line":292},[249,1335,1336],{"class":254},"while",[249,1338,1339],{"class":258}," current ",[249,1341,1342],{"class":254},"!=",[249,1344,1345],{"class":258}," start_vertex:\n",[249,1347,1348,1351,1353],{"class":97,"line":305},[249,1349,1350],{"class":258},"    edge_id ",[249,1352,318],{"class":254},[249,1354,1355],{"class":258}," tree[current]\n",[249,1357,1358,1361,1364,1367,1370,1372],{"class":97,"line":312},[249,1359,1360],{"class":254},"    if",[249,1362,1363],{"class":258}," edge_id ",[249,1365,1366],{"class":254},"==",[249,1368,1369],{"class":254}," -",[249,1371,365],{"class":331},[249,1373,1374],{"class":258},":\n",[249,1376,1377,1380,1383,1385,1388],{"class":97,"line":337},[249,1378,1379],{"class":254},"        raise",[249,1381,1382],{"class":331}," SystemExit",[249,1384,897],{"class":258},[249,1386,1387],{"class":324},"\"destination is not reachable from the start\"",[249,1389,289],{"class":258},[249,1391,1392,1395,1397],{"class":97,"line":342},[249,1393,1394],{"class":258},"    current ",[249,1396,318],{"class":254},[249,1398,1399],{"class":258}," graph.edge(edge_id).fromVertex()\n",[249,1401,1402,1405,1407],{"class":97,"line":353},[249,1403,1404],{"class":258},"    route.insert(",[249,1406,46],{"class":331},[249,1408,1409],{"class":258},", graph.vertex(current).point())\n",[249,1411,1412],{"class":97,"line":359},[249,1413,309],{"emptyLinePlaceholder":308},[249,1415,1416,1419,1421],{"class":97,"line":375},[249,1417,1418],{"class":258},"geometry ",[249,1420,318],{"class":254},[249,1422,1423],{"class":258}," QgsGeometry.fromPolylineXY(route)\n",[249,1425,1426,1428,1430,1433,1436,1439,1442,1445,1448,1451,1454,1457,1460,1463,1466],{"class":97,"line":386},[249,1427,894],{"class":331},[249,1429,897],{"class":258},[249,1431,1432],{"class":254},"f",[249,1434,1435],{"class":324},"\"",[249,1437,1438],{"class":331},"{",[249,1440,1441],{"class":258},"geometry.length()",[249,1443,1444],{"class":254},":.0f",[249,1446,1447],{"class":331},"}",[249,1449,1450],{"class":324}," m along ",[249,1452,1453],{"class":331},"{len",[249,1455,1456],{"class":258},"(route) ",[249,1458,1459],{"class":254},"-",[249,1461,1462],{"class":331}," 1}",[249,1464,1465],{"class":324}," edges\"",[249,1467,289],{"class":258},[14,1469,1470,1472,1473,1475,1476,944,1479,1482,1483,944,1486,1489,1490,482],{},[462,1471,464],{}," The loop walks from the destination back to the origin, inserting each vertex at the front so the finished list runs the right way. The ",[202,1474,468],{}," check is not optional — without it, an unreachable destination produces an infinite loop rather than an error, which is a genuinely unpleasant way to discover a disconnected network. ",[202,1477,1478],{},"fromVertex()",[202,1480,1481],{},"toVertex()"," are the current accessor names; QGIS versions before 3.24 called them ",[202,1484,1485],{},"inVertex()",[202,1487,1488],{},"outVertex()",", and code that has to span both should branch on ",[202,1491,1492],{},"hasattr",[14,1494,1495],{},"Note what this geometry is and is not. It is a polyline through graph vertices — that is, through the endpoints of the original line features — so it follows the network's shape only as closely as those endpoints describe it. Where the source lines are long curved segments the route cuts the corners, because the intermediate vertices of each source line are not in the graph at all. If you need the true drawn geometry, use the Processing algorithms, which reconstruct from the source features rather than from graph vertices.",[194,1497,1499],{"id":1498},"preparing-a-layer-that-will-actually-route","Preparing a layer that will actually route",[14,1501,1502],{},"Most routing failures are data failures, and they show up as an unreachable destination rather than as an error. Three preparation steps cover almost all of them.",[14,1504,1505],{},"Split lines at every intersection so junctions become shared endpoints. Reproject to a metric CRS so tolerances and lengths are in metres rather than degrees — routing in EPSG:4326 makes every tolerance meaningless. Then check connectivity before you trust anything: run a service area from one point with a very large budget and see how much of the network it reaches. If it reaches a third of the roads, the network is in fragments.",[14,1507,1508],{},"Checking connectivity deserves a concrete recipe, because \"does this network route\" is the question you want answered before you build anything on top of it.",[240,1510,1512],{"className":242,"code":1511,"language":244,"meta":245,"style":245},"reachable = sum(1 for cost in costs if cost \u003C 1e10)\nprint(f\"{reachable} of {graph.vertexCount()} vertices reachable \"\n      f\"({100 * reachable \u002F graph.vertexCount():.0f}%)\")\n",[202,1513,1514,1553,1583],{"__ignoreMap":245},[249,1515,1516,1519,1521,1524,1526,1528,1531,1534,1537,1540,1543,1545,1548,1551],{"class":97,"line":251},[249,1517,1518],{"class":258},"reachable ",[249,1520,318],{"class":254},[249,1522,1523],{"class":331}," sum",[249,1525,897],{"class":258},[249,1527,365],{"class":331},[249,1529,1530],{"class":254}," for",[249,1532,1533],{"class":258}," cost ",[249,1535,1536],{"class":254},"in",[249,1538,1539],{"class":258}," costs ",[249,1541,1542],{"class":254},"if",[249,1544,1533],{"class":258},[249,1546,1547],{"class":254},"\u003C",[249,1549,1550],{"class":331}," 1e10",[249,1552,289],{"class":258},[249,1554,1555,1557,1559,1561,1563,1565,1568,1570,1573,1575,1578,1580],{"class":97,"line":268},[249,1556,894],{"class":331},[249,1558,897],{"class":258},[249,1560,1432],{"class":254},[249,1562,1435],{"class":324},[249,1564,1438],{"class":331},[249,1566,1567],{"class":258},"reachable",[249,1569,1447],{"class":331},[249,1571,1572],{"class":324}," of ",[249,1574,1438],{"class":331},[249,1576,1577],{"class":258},"graph.vertexCount()",[249,1579,1447],{"class":331},[249,1581,1582],{"class":324}," vertices reachable \"\n",[249,1584,1585,1588,1591,1594,1597,1600,1603,1606,1608,1610,1613],{"class":97,"line":274},[249,1586,1587],{"class":254},"      f",[249,1589,1590],{"class":324},"\"(",[249,1592,1593],{"class":331},"{100",[249,1595,1596],{"class":254}," *",[249,1598,1599],{"class":258}," reachable ",[249,1601,1602],{"class":254},"\u002F",[249,1604,1605],{"class":258}," graph.vertexCount()",[249,1607,1444],{"class":254},[249,1609,1447],{"class":331},[249,1611,1612],{"class":324},"%)\"",[249,1614,289],{"class":258},[14,1616,1617,1619,1620,1623],{},[462,1618,464],{}," Running Dijkstra once from a central point and counting what it reaches is the cheapest possible connectivity audit. On a properly noded network in a single connected component this prints close to 100%; anything below about 90% means the layer is in fragments, and the fragments are usually visible immediately if you write the unreachable vertices out as a point layer and look at where they cluster. The threshold of ",[202,1621,1622],{},"1e10"," is arbitrary but safe — the sentinel used for unreachable vertices is far larger than any real cost on a terrestrial network.",[14,1625,1626,1627,1631],{},"The other preparation worth doing once is a spatial subset. A graph over a whole country to answer questions inside one city wastes minutes of build time and gigabytes of memory for no benefit. Clip the network to the study area plus a generous buffer — generous meaning \"wider than the longest route you will ask for\", because a route clipped at the boundary will detour absurdly rather than leave the buffer. ",[21,1628,1630],{"href":1629},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","Clipping a vector layer"," covers the mechanics; the judgement about how much buffer is enough is yours.",[194,1633,1635],{"id":1634},"key-takeaways","Key takeaways",[1637,1638,1639,1647,1650,1653,1658,1663,1666],"ul",{},[1640,1641,1642,1643,1646],"li",{},"A graph is built from line ",[207,1644,1645],{},"endpoints",". Split your lines at junctions or nothing will connect.",[1640,1648,1649],{},"Set a topology tolerance appropriate to the data's precision, in a metric CRS, and never route in degrees.",[1640,1651,1652],{},"Direction comes from an attribute compared as an exact string; everything unmatched falls through to the default.",[1640,1654,1655,1657],{},[202,1656,733],{}," needs a unit multiplier, and getting it wrong scales every time by a constant.",[1640,1659,1660,1662],{},[202,1661,911],{}," returns a predecessor tree and a cost array; routes and service areas are two readings of the same result.",[1640,1664,1665],{},"Use the Processing algorithms for single answers and the graph API when many queries share one network.",[1640,1667,1668],{},"Turn restrictions are not modelled. If they matter, use a dedicated routing engine.",[194,1670,1672],{"id":1671},"frequently-asked-questions","Frequently Asked Questions",[14,1674,1675,1678],{},[462,1676,1677],{},"Why does my route ignore a road that is clearly connected?","\nIts endpoints do not coincide with the neighbouring road's endpoints within the tolerance, or the two roads cross without a shared vertex. Split the layer at intersections and raise the tolerance to match your data's digitising precision.",[14,1680,1681,1684,1685,1688,1689,1693],{},[462,1682,1683],{},"Can I route on a network stored in PostGIS?","\nYes — the director takes any ",[202,1686,1687],{},"QgsVectorLayer",", so a PostGIS layer works exactly like a file-based one, and reading it is covered in ",[21,1690,1692],{"href":1691},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fconnect-to-postgis-database-pyqgis\u002F","connecting to a PostGIS database",". For very large networks, a database-side routing extension will outperform building a graph in memory.",[14,1695,1696,1699],{},[462,1697,1698],{},"How large a network can QGIS handle?","\nThe graph is held in memory with a vertex per endpoint and an edge per segment direction, so a few hundred thousand edges is comfortable and a few million is not. Clip the network to a generous buffer around your area of interest first.",[14,1701,1702,1705,1707],{},[462,1703,1704],{},"Does the analysis use the ellipsoid for distances?",[202,1706,204],{}," takes an ellipsoid identifier and will compute geodesic lengths when given one, but the sane approach is to work in a projected CRS where planar lengths are already correct for the area.",[14,1709,1710,1713,1714,1717],{},[462,1711,1712],{},"Can I get a route that visits several stops in order?","\nRun consecutive point-to-point queries against the same graph and concatenate. Optimising the ",[207,1715,1716],{},"order"," of the stops is the travelling salesman problem, which QGIS does not solve for you.",[14,1719,1720,1723,1724,1727],{},[462,1721,1722],{},"Why are all my costs enormous numbers?","\nThose are the unreachable vertices. Dijkstra fills them with a sentinel rather than infinity, so filter on reachability using ",[202,1725,1726],{},"tree[v] == -1"," instead of comparing costs against a threshold.",[194,1729,1731],{"id":1730},"related","Related",[1637,1733,1734,1740,1746,1751,1756,1761,1766,1772],{},[1640,1735,1736,1739],{},[21,1737,1738],{"href":23},"Spatial Data Processing & Automation with PyQGIS"," — the section this guide belongs to",[1640,1741,1742],{},[21,1743,1745],{"href":1744},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder\u002F","Build a Network Graph with QgsGraphBuilder",[1640,1747,1748],{},[21,1749,1750],{"href":942},"Find the Shortest Path Between Points in PyQGIS",[1640,1752,1753],{},[21,1754,1755],{"href":947},"Calculate Service Areas in PyQGIS",[1640,1757,1758],{},[21,1759,1760],{"href":807},"Add Speed and Travel Cost to a Network in PyQGIS",[1640,1762,1763],{},[21,1764,1765],{"href":480},"Snap Points to a Network in PyQGIS",[1640,1767,1768],{},[21,1769,1771],{"href":1770},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-origin-destination-matrix-pyqgis\u002F","Build an Origin-Destination Matrix in PyQGIS",[1640,1773,1774],{},[21,1775,1777],{"href":1776},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations & Predicates in PyQGIS",[1779,1780,1781],"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);}",{"title":245,"searchDepth":268,"depth":268,"links":1783},[1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795],{"id":196,"depth":268,"text":197},{"id":237,"depth":268,"text":238},{"id":504,"depth":268,"text":505},{"id":722,"depth":268,"text":723},{"id":819,"depth":268,"text":820},{"id":952,"depth":268,"text":953},{"id":1010,"depth":268,"text":1011},{"id":1283,"depth":268,"text":1284},{"id":1498,"depth":268,"text":1499},{"id":1634,"depth":268,"text":1635},{"id":1671,"depth":268,"text":1672},{"id":1730,"depth":268,"text":1731},"Build a routable graph from a line layer with QgsGraphBuilder, run shortest-path and service-area analysis, model one-way streets and travel speeds, and get snapping right.","md",{"slug":1799,"type":1800,"breadcrumb":1801,"datePublished":1802,"dateModified":1802},"network-analysis-and-routing","guide","Network Analysis & Routing","2026-09-05","\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing",{"title":5,"description":1796},"spatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Findex","W-0QfQEwAKGsPMApFM1HAA-oNbctwsD4Z6Xb-BOK9yw",1788563855487]