[{"data":1,"prerenderedAt":1392},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-vector-tile-layer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1381,"extension":1382,"meta":1383,"navigation":235,"path":1388,"seo":1389,"stem":1390,"__hash__":1391},"docs\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-vector-tile-layer-pyqgis\u002Findex.md","Load a Vector Tile Layer in PyQGIS",{"type":7,"value":8,"toc":1367},"minimark",[9,13,17,26,155,160,177,181,189,386,419,425,429,432,496,508,512,515,592,606,609,746,750,753,1004,1026,1038,1042,1045,1113,1118,1122,1125,1139,1147,1151,1157,1240,1244,1291,1295,1298,1302,1308,1314,1320,1330,1334,1363],[10,11,5],"h1",{"id":12},"load-a-vector-tile-layer-in-pyqgis",[14,15,16],"p",{},"Vector tiles sit between a raster basemap and a real vector layer. Like a raster basemap they arrive as pre-cut tiles over HTTP and cost nothing to render at continental scale; unlike one, the tiles contain geometry, so the map can be restyled, labelled in the local language and drawn crisply at any zoom. What they are not is a data source you can run analysis against, and treating them as one is the mistake worth avoiding early.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002F","Web Services & Remote Data in PyQGIS",". It covers building the URI for XYZ and MBTiles sources, setting the zoom range, applying a MapBox style JSON, reading what is actually in a tile, and where vector tiles stop being appropriate.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,86,90,94,97,102,107,110,113,117,120,123,127,129,133,137,140,143,146,149,152],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 316","img","A comparison of raster tiles, vector tiles and a real vector layer across styling, labelling, attribute access and suitability for analysis","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Where vector tiles sit",[40,41,42],"desc",{},"Raster tiles are pixels: fast, fixed styling, no attributes. Vector tiles carry generalised geometry per zoom level, so they restyle and label freely but the geometry is simplified per tile and clipped at tile boundaries. A real vector layer carries full geometry and attributes and is the only one of the three suitable for analysis.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","316","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Restylable is not the same as analysable",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"20","52","228","222","10","#fdf2e2","#b45309","stroke-width:2.5",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"134","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","raster tiles",[51,76,80],{"x":71,"y":77,"style":78,"fill":79,"textAnchor":57},"110","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","pixels over HTTP",[51,82,85],{"x":71,"y":83,"style":78,"fill":84,"textAnchor":57},"136","#b91c1c","styling fixed by the server",[51,87,89],{"x":71,"y":88,"style":78,"fill":84,"textAnchor":57},"162","no attributes at all",[51,91,93],{"x":71,"y":92,"style":78,"fill":84,"textAnchor":57},"188","blurs between zoom levels",[51,95,96],{"x":71,"y":64,"style":78,"fill":79,"textAnchor":57},"cheapest basemap",[51,98,101],{"x":71,"y":99,"style":100,"fill":67,"textAnchor":57},"248","text-anchor:middle;font-size:10px;font-family:sans-serif","no analysis",[44,103],{"x":104,"y":62,"width":63,"height":64,"rx":65,"fill":105,"stroke":106,"style":68},"266","#eff3ff","#2563eb",[51,108,109],{"x":53,"y":72,"style":73,"fill":106,"textAnchor":57},"vector tiles",[51,111,112],{"x":53,"y":77,"style":78,"fill":79,"textAnchor":57},"geometry over HTTP",[51,114,116],{"x":53,"y":83,"style":78,"fill":115,"textAnchor":57},"#15803d","restyle and relabel freely",[51,118,119],{"x":53,"y":88,"style":78,"fill":115,"textAnchor":57},"crisp at every zoom",[51,121,122],{"x":53,"y":92,"style":78,"fill":84,"textAnchor":57},"generalised per zoom level",[51,124,126],{"x":53,"y":125,"style":78,"fill":84,"textAnchor":57},"214","clipped at tile edges",[51,128,101],{"x":53,"y":99,"style":100,"fill":67,"textAnchor":57},[44,130],{"x":131,"y":62,"width":63,"height":64,"rx":65,"fill":132,"stroke":115,"style":68},"512","#edf8e9",[51,134,136],{"x":135,"y":72,"style":73,"fill":115,"textAnchor":57},"626","real vector layer",[51,138,139],{"x":135,"y":77,"style":78,"fill":79,"textAnchor":57},"full geometry and fields",[51,141,142],{"x":135,"y":83,"style":78,"fill":115,"textAnchor":57},"every feature complete",[51,144,145],{"x":135,"y":88,"style":78,"fill":115,"textAnchor":57},"queryable and editable",[51,147,148],{"x":135,"y":92,"style":78,"fill":67,"textAnchor":57},"heavier to load and draw",[51,150,151],{"x":135,"y":64,"style":78,"fill":79,"textAnchor":57},"the only analysable one",[51,153,154],{"x":135,"y":99,"style":100,"fill":115,"textAnchor":57},"use WFS or a download",[156,157,159],"h2",{"id":158},"prerequisites","Prerequisites",[161,162,163,171,174],"ul",{},[164,165,166,170],"li",{},[167,168,169],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer. Vector tile support arrived in 3.14 and matured through 3.16 to 3.20.",[164,172,173],{},"A tile source: an XYZ template URL, or an MBTiles file. Many public services require an API key in the URL.",[164,175,176],{},"Network access, or a local MBTiles file for offline work.",[156,178,180],{"id":179},"load-an-xyz-vector-tile-source","Load an XYZ vector tile source",[14,182,183,184,188],{},"The layer is created from a URI string of ",[185,186,187],"code",{},"key=value"," pairs, and the encoding rules matter.",[190,191,196],"pre",{"className":192,"code":193,"language":194,"meta":195,"style":195},"language-python shiki shiki-themes github-dark","from qgis.core import QgsVectorTileLayer, QgsProject\nfrom urllib.parse import quote\n\ntemplate = \"https:\u002F\u002Ftiles.example.org\u002Fdata\u002Fv3\u002F{z}\u002F{x}\u002F{y}.pbf?key=YOUR_KEY\"\nuri = (\n    \"type=xyz\"\n    f\"&url={quote(template, safe='')}\"\n    \"&zmin=0&zmax=14\"\n)\n\nlayer = QgsVectorTileLayer(uri, \"Base tiles\")\nif not layer.isValid():\n    raise RuntimeError(\"vector tile layer failed to load — check the URL and key\")\nQgsProject.instance().addMapLayer(layer)\n","python","",[185,197,198,217,230,237,268,279,285,318,324,330,335,351,363,380],{"__ignoreMap":195},[199,200,203,207,211,214],"span",{"class":201,"line":202},"line",1,[199,204,206],{"class":205},"snl16","from",[199,208,210],{"class":209},"s95oV"," qgis.core ",[199,212,213],{"class":205},"import",[199,215,216],{"class":209}," QgsVectorTileLayer, QgsProject\n",[199,218,220,222,225,227],{"class":201,"line":219},2,[199,221,206],{"class":205},[199,223,224],{"class":209}," urllib.parse ",[199,226,213],{"class":205},[199,228,229],{"class":209}," quote\n",[199,231,233],{"class":201,"line":232},3,[199,234,236],{"emptyLinePlaceholder":235},true,"\n",[199,238,240,243,246,250,254,257,260,262,265],{"class":201,"line":239},4,[199,241,242],{"class":209},"template ",[199,244,245],{"class":205},"=",[199,247,249],{"class":248},"sU2Wk"," \"https:\u002F\u002Ftiles.example.org\u002Fdata\u002Fv3\u002F",[199,251,253],{"class":252},"sDLfK","{z}",[199,255,256],{"class":248},"\u002F",[199,258,259],{"class":252},"{x}",[199,261,256],{"class":248},[199,263,264],{"class":252},"{y}",[199,266,267],{"class":248},".pbf?key=YOUR_KEY\"\n",[199,269,271,274,276],{"class":201,"line":270},5,[199,272,273],{"class":209},"uri ",[199,275,245],{"class":205},[199,277,278],{"class":209}," (\n",[199,280,282],{"class":201,"line":281},6,[199,283,284],{"class":248},"    \"type=xyz\"\n",[199,286,288,291,294,297,300,304,306,309,312,315],{"class":201,"line":287},7,[199,289,290],{"class":205},"    f",[199,292,293],{"class":248},"\"&url=",[199,295,296],{"class":252},"{",[199,298,299],{"class":209},"quote(template, ",[199,301,303],{"class":302},"s9osk","safe",[199,305,245],{"class":205},[199,307,308],{"class":248},"''",[199,310,311],{"class":209},")",[199,313,314],{"class":252},"}",[199,316,317],{"class":248},"\"\n",[199,319,321],{"class":201,"line":320},8,[199,322,323],{"class":248},"    \"&zmin=0&zmax=14\"\n",[199,325,327],{"class":201,"line":326},9,[199,328,329],{"class":209},")\n",[199,331,333],{"class":201,"line":332},10,[199,334,236],{"emptyLinePlaceholder":235},[199,336,338,341,343,346,349],{"class":201,"line":337},11,[199,339,340],{"class":209},"layer ",[199,342,245],{"class":205},[199,344,345],{"class":209}," QgsVectorTileLayer(uri, ",[199,347,348],{"class":248},"\"Base tiles\"",[199,350,329],{"class":209},[199,352,354,357,360],{"class":201,"line":353},12,[199,355,356],{"class":205},"if",[199,358,359],{"class":205}," not",[199,361,362],{"class":209}," layer.isValid():\n",[199,364,366,369,372,375,378],{"class":201,"line":365},13,[199,367,368],{"class":205},"    raise",[199,370,371],{"class":252}," RuntimeError",[199,373,374],{"class":209},"(",[199,376,377],{"class":248},"\"vector tile layer failed to load — check the URL and key\"",[199,379,329],{"class":209},[199,381,383],{"class":201,"line":382},14,[199,384,385],{"class":209},"QgsProject.instance().addMapLayer(layer)\n",[14,387,388,391,392,395,396,399,400,403,404,406,407,395,409,411,412,395,415,418],{},[167,389,390],{},"Breakdown:"," The template URL must be percent-encoded when embedded in the URI, because it contains ",[185,393,394],{},"&"," and ",[185,397,398],{},"?"," characters that would otherwise be read as URI separators — ",[185,401,402],{},"quote(..., safe='')"," handles that, and forgetting it produces a layer that reports itself valid and fetches nothing. The ",[185,405,253],{},", ",[185,408,259],{},[185,410,264],{}," placeholders survive the encoding as literal braces, which is what the provider expects. ",[185,413,414],{},"zmin",[185,416,417],{},"zmax"," must match what the service actually publishes; requesting zoom 18 from a source that stops at 14 produces empty tiles rather than an error.",[14,420,421,424],{},[185,422,423],{},"isValid()"," on a vector tile layer is weaker than on a file-based layer — it confirms the URI parsed, not that any tile was fetched. A layer that draws nothing with a valid URI usually means a wrong key or a wrong zoom range, and the network log is where the answer is.",[156,426,428],{"id":427},"load-an-mbtiles-file","Load an MBTiles file",[14,430,431],{},"An MBTiles container holds the same tiles offline, which is the right choice for a field laptop or a reproducible script.",[190,433,435],{"className":192,"code":434,"language":194,"meta":195,"style":195},"uri = \"type=mbtiles&url=\u002Fdata\u002Ftiles\u002Fregion.mbtiles\"\nlayer = QgsVectorTileLayer(uri, \"Region tiles\")\nif not layer.isValid():\n    raise RuntimeError(\"MBTiles source did not load\")\nQgsProject.instance().addMapLayer(layer)\n\nprint(layer.sourceMinZoom(), layer.sourceMaxZoom())\n",[185,436,437,446,459,467,480,484,488],{"__ignoreMap":195},[199,438,439,441,443],{"class":201,"line":202},[199,440,273],{"class":209},[199,442,245],{"class":205},[199,444,445],{"class":248}," \"type=mbtiles&url=\u002Fdata\u002Ftiles\u002Fregion.mbtiles\"\n",[199,447,448,450,452,454,457],{"class":201,"line":219},[199,449,340],{"class":209},[199,451,245],{"class":205},[199,453,345],{"class":209},[199,455,456],{"class":248},"\"Region tiles\"",[199,458,329],{"class":209},[199,460,461,463,465],{"class":201,"line":232},[199,462,356],{"class":205},[199,464,359],{"class":205},[199,466,362],{"class":209},[199,468,469,471,473,475,478],{"class":201,"line":239},[199,470,368],{"class":205},[199,472,371],{"class":252},[199,474,374],{"class":209},[199,476,477],{"class":248},"\"MBTiles source did not load\"",[199,479,329],{"class":209},[199,481,482],{"class":201,"line":270},[199,483,385],{"class":209},[199,485,486],{"class":201,"line":281},[199,487,236],{"emptyLinePlaceholder":235},[199,489,490,493],{"class":201,"line":287},[199,491,492],{"class":252},"print",[199,494,495],{"class":209},"(layer.sourceMinZoom(), layer.sourceMaxZoom())\n",[14,497,498,500,501,395,504,507],{},[167,499,390],{}," No encoding is needed for a plain file path with no query string. ",[185,502,503],{},"sourceMinZoom()",[185,505,506],{},"sourceMaxZoom()"," read the range from the container's metadata table, which is the reliable way to discover it rather than guessing — and worth printing the first time a new file appears, because a mismatch between the declared range and the requested one is the commonest cause of a blank map.",[156,509,511],{"id":510},"apply-a-style","Apply a style",[14,513,514],{},"Out of the box a vector tile layer draws with a default style that is deliberately plain. Real styling comes from a MapBox GL style JSON or from QGIS's own renderer.",[190,516,518],{"className":192,"code":517,"language":194,"meta":195,"style":195},"message, ok = layer.loadDefaultStyle()\nif not ok:\n    message, ok = layer.loadNamedStyle(\"\u002Fdata\u002Fstyles\u002Fbasemap.json\")\nif not ok:\n    print(f\"style not applied: {message}\")\nlayer.triggerRepaint()\n",[185,519,520,530,539,554,562,587],{"__ignoreMap":195},[199,521,522,525,527],{"class":201,"line":202},[199,523,524],{"class":209},"message, ok ",[199,526,245],{"class":205},[199,528,529],{"class":209}," layer.loadDefaultStyle()\n",[199,531,532,534,536],{"class":201,"line":219},[199,533,356],{"class":205},[199,535,359],{"class":205},[199,537,538],{"class":209}," ok:\n",[199,540,541,544,546,549,552],{"class":201,"line":232},[199,542,543],{"class":209},"    message, ok ",[199,545,245],{"class":205},[199,547,548],{"class":209}," layer.loadNamedStyle(",[199,550,551],{"class":248},"\"\u002Fdata\u002Fstyles\u002Fbasemap.json\"",[199,553,329],{"class":209},[199,555,556,558,560],{"class":201,"line":239},[199,557,356],{"class":205},[199,559,359],{"class":205},[199,561,538],{"class":209},[199,563,564,567,569,572,575,577,580,582,585],{"class":201,"line":270},[199,565,566],{"class":252},"    print",[199,568,374],{"class":209},[199,570,571],{"class":205},"f",[199,573,574],{"class":248},"\"style not applied: ",[199,576,296],{"class":252},[199,578,579],{"class":209},"message",[199,581,314],{"class":252},[199,583,584],{"class":248},"\"",[199,586,329],{"class":209},[199,588,589],{"class":201,"line":281},[199,590,591],{"class":209},"layer.triggerRepaint()\n",[14,593,594,596,597,600,601,605],{},[167,595,390],{}," ",[185,598,599],{},"loadDefaultStyle()"," picks up a style the service advertises, which many tile providers do. Falling back to a named style file lets a project pin its own appearance; QGIS reads MapBox GL JSON and converts it into its own rule-based renderer, which is a genuine conversion rather than a passthrough — expressions and sprite-based symbols do not always survive. Checking the boolean rather than assuming is the same discipline as ",[21,602,604],{"href":603},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fapply-sld-style-to-layer-pyqgis\u002F","applying an SLD style",".",[14,607,608],{},"Once converted, the renderer is an ordinary QGIS object and can be edited from Python — each sub-layer of the tile source becomes a rule with its own filter, so restyling roads without touching buildings is a matter of finding the right rule.",[14,610,611],{},[29,612,615,618,621,624,640,643,649,655,664,669,671,675,678,682,685,689,694,697,701,705,708,710,713,715,718,722,725,729,737,742],{"viewBox":613,"role":32,"ariaLabel":614,"xmlns":34},"0 0 760 292","One vector tile source containing several named sub-layers, each becoming a rule in the QGIS renderer so it can be styled or hidden independently",[36,616,617],{},"Sub-layers inside one tile source",[40,619,620],{},"A single vector tile source carries several named sub layers such as water, landuse, roads and place labels. When a style is applied each sub layer becomes a rule in the layer's rule based renderer, so any one of them can be restyled, filtered or switched off from Python without affecting the others.",[44,622],{"x":46,"y":46,"width":47,"height":623,"fill":49},"292",[625,626,627],"defs",{},[628,629,636],"marker",{"id":630,"viewBox":631,"refX":632,"refY":633,"markerWidth":634,"markerHeight":634,"orient":635},"vtArrow","0 0 10 10","8","5","7","auto-start-reverse",[637,638],"path",{"d":639,"fill":79},"M0 0 L10 5 L0 10 z",[51,641,642],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"One layer in the tree, many rules underneath",[44,644],{"x":645,"y":646,"width":647,"height":648,"rx":65,"fill":105,"stroke":106,"style":68},"30","60","230","190",[51,650,654],{"x":651,"y":652,"style":653,"fill":106,"textAnchor":57},"145","88","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","one tile",[44,656],{"x":657,"y":658,"width":659,"height":660,"rx":661,"fill":662,"stroke":106,"style":663},"54","104","182","26","4","#dbeafe","stroke-width:1.4",[51,665,668],{"x":651,"y":666,"style":667,"fill":79,"textAnchor":57},"122","text-anchor:middle;font-size:10px;font-family:monospace","water",[44,670],{"x":657,"y":83,"width":659,"height":660,"rx":661,"fill":662,"stroke":106,"style":663},[51,672,674],{"x":651,"y":673,"style":667,"fill":79,"textAnchor":57},"154","landuse",[44,676],{"x":657,"y":677,"width":659,"height":660,"rx":661,"fill":662,"stroke":106,"style":663},"168",[51,679,681],{"x":651,"y":680,"style":667,"fill":79,"textAnchor":57},"186","transportation",[44,683],{"x":657,"y":684,"width":659,"height":660,"rx":661,"fill":662,"stroke":106,"style":663},"200",[51,686,688],{"x":651,"y":687,"style":667,"fill":79,"textAnchor":57},"218","place",[201,690],{"x1":104,"y1":691,"x2":692,"y2":691,"stroke":79,"style":693},"155","304","stroke-width:2;marker-end:url(#vtArrow)",[44,695],{"x":696,"y":646,"width":647,"height":648,"rx":65,"fill":132,"stroke":115,"style":68},"312",[51,698,700],{"x":699,"y":652,"style":653,"fill":115,"textAnchor":57},"427","renderer rules",[44,702],{"x":703,"y":658,"width":659,"height":660,"rx":661,"fill":704,"stroke":115,"style":663},"336","#e8efe6",[51,706,707],{"x":699,"y":666,"style":100,"fill":79,"textAnchor":57},"water — blue fill",[44,709],{"x":703,"y":83,"width":659,"height":660,"rx":661,"fill":704,"stroke":115,"style":663},[51,711,712],{"x":699,"y":673,"style":100,"fill":79,"textAnchor":57},"landuse — muted fills",[44,714],{"x":703,"y":677,"width":659,"height":660,"rx":661,"fill":704,"stroke":115,"style":663},[51,716,717],{"x":699,"y":680,"style":100,"fill":79,"textAnchor":57},"roads — casing + fill",[44,719],{"x":703,"y":684,"width":659,"height":660,"rx":661,"fill":720,"stroke":721,"style":663},"#efeadd","#59645f",[51,723,724],{"x":699,"y":687,"style":100,"fill":721,"textAnchor":57},"place — disabled",[201,726],{"x1":727,"y1":691,"x2":728,"y2":691,"stroke":79,"style":693},"548","586",[44,730],{"x":731,"y":732,"width":733,"height":734,"rx":632,"fill":735,"stroke":736,"style":68},"594","112","140","86","#fffdf7","#0f766e",[51,738,741],{"x":739,"y":740,"style":78,"fill":79,"textAnchor":57},"664","144","restyle any rule",[51,743,745],{"x":739,"y":744,"style":78,"fill":79,"textAnchor":57},"166","from Python",[156,747,749],{"id":748},"labelling-from-tile-attributes","Labelling from tile attributes",[14,751,752],{},"The single best reason to prefer vector tiles over a raster basemap is that the labels are yours. A raster basemap ships place names baked into the pixels in one language at one size; vector tiles ship the names as attributes.",[190,754,756],{"className":192,"code":755,"language":194,"meta":195,"style":195},"from qgis.core import (\n    QgsPalLayerSettings, QgsTextFormat, QgsVectorTileBasicLabeling,\n    QgsVectorTileBasicLabelingStyle, QgsWkbTypes,\n)\nfrom qgis.PyQt.QtGui import QColor\n\nsettings = QgsPalLayerSettings()\nsettings.fieldName = \"coalesce(\\\"name:cy\\\", \\\"name\\\")\"\nsettings.isExpression = True\nsettings.placement = QgsPalLayerSettings.OverPoint\n\nfmt = QgsTextFormat()\nfmt.setSize(9)\nfmt.setColor(QColor(\"#17211d\"))\nsettings.setFormat(fmt)\n\nstyle = QgsVectorTileBasicLabelingStyle()\nstyle.setStyleName(\"places\")\nstyle.setLayerName(\"place\")\nstyle.setGeometryType(QgsWkbTypes.PointGeometry)\nstyle.setMinZoomLevel(6)\nstyle.setMaxZoomLevel(14)\nstyle.setLabelSettings(settings)\n\nlabeling = QgsVectorTileBasicLabeling()\nlabeling.setStyles([style])\nlayer.setLabeling(labeling)\nlayer.triggerRepaint()\n",[185,757,758,768,773,778,782,794,798,808,838,848,858,862,872,882,893,899,904,915,926,937,943,954,965,971,976,987,993,999],{"__ignoreMap":195},[199,759,760,762,764,766],{"class":201,"line":202},[199,761,206],{"class":205},[199,763,210],{"class":209},[199,765,213],{"class":205},[199,767,278],{"class":209},[199,769,770],{"class":201,"line":219},[199,771,772],{"class":209},"    QgsPalLayerSettings, QgsTextFormat, QgsVectorTileBasicLabeling,\n",[199,774,775],{"class":201,"line":232},[199,776,777],{"class":209},"    QgsVectorTileBasicLabelingStyle, QgsWkbTypes,\n",[199,779,780],{"class":201,"line":239},[199,781,329],{"class":209},[199,783,784,786,789,791],{"class":201,"line":270},[199,785,206],{"class":205},[199,787,788],{"class":209}," qgis.PyQt.QtGui ",[199,790,213],{"class":205},[199,792,793],{"class":209}," QColor\n",[199,795,796],{"class":201,"line":281},[199,797,236],{"emptyLinePlaceholder":235},[199,799,800,803,805],{"class":201,"line":287},[199,801,802],{"class":209},"settings ",[199,804,245],{"class":205},[199,806,807],{"class":209}," QgsPalLayerSettings()\n",[199,809,810,813,815,818,821,824,826,828,830,833,835],{"class":201,"line":320},[199,811,812],{"class":209},"settings.fieldName ",[199,814,245],{"class":205},[199,816,817],{"class":248}," \"coalesce(",[199,819,820],{"class":252},"\\\"",[199,822,823],{"class":248},"name:cy",[199,825,820],{"class":252},[199,827,406],{"class":248},[199,829,820],{"class":252},[199,831,832],{"class":248},"name",[199,834,820],{"class":252},[199,836,837],{"class":248},")\"\n",[199,839,840,843,845],{"class":201,"line":326},[199,841,842],{"class":209},"settings.isExpression ",[199,844,245],{"class":205},[199,846,847],{"class":252}," True\n",[199,849,850,853,855],{"class":201,"line":332},[199,851,852],{"class":209},"settings.placement ",[199,854,245],{"class":205},[199,856,857],{"class":209}," QgsPalLayerSettings.OverPoint\n",[199,859,860],{"class":201,"line":337},[199,861,236],{"emptyLinePlaceholder":235},[199,863,864,867,869],{"class":201,"line":353},[199,865,866],{"class":209},"fmt ",[199,868,245],{"class":205},[199,870,871],{"class":209}," QgsTextFormat()\n",[199,873,874,877,880],{"class":201,"line":365},[199,875,876],{"class":209},"fmt.setSize(",[199,878,879],{"class":252},"9",[199,881,329],{"class":209},[199,883,884,887,890],{"class":201,"line":382},[199,885,886],{"class":209},"fmt.setColor(QColor(",[199,888,889],{"class":248},"\"#17211d\"",[199,891,892],{"class":209},"))\n",[199,894,896],{"class":201,"line":895},15,[199,897,898],{"class":209},"settings.setFormat(fmt)\n",[199,900,902],{"class":201,"line":901},16,[199,903,236],{"emptyLinePlaceholder":235},[199,905,907,910,912],{"class":201,"line":906},17,[199,908,909],{"class":209},"style ",[199,911,245],{"class":205},[199,913,914],{"class":209}," QgsVectorTileBasicLabelingStyle()\n",[199,916,918,921,924],{"class":201,"line":917},18,[199,919,920],{"class":209},"style.setStyleName(",[199,922,923],{"class":248},"\"places\"",[199,925,329],{"class":209},[199,927,929,932,935],{"class":201,"line":928},19,[199,930,931],{"class":209},"style.setLayerName(",[199,933,934],{"class":248},"\"place\"",[199,936,329],{"class":209},[199,938,940],{"class":201,"line":939},20,[199,941,942],{"class":209},"style.setGeometryType(QgsWkbTypes.PointGeometry)\n",[199,944,946,949,952],{"class":201,"line":945},21,[199,947,948],{"class":209},"style.setMinZoomLevel(",[199,950,951],{"class":252},"6",[199,953,329],{"class":209},[199,955,957,960,963],{"class":201,"line":956},22,[199,958,959],{"class":209},"style.setMaxZoomLevel(",[199,961,962],{"class":252},"14",[199,964,329],{"class":209},[199,966,968],{"class":201,"line":967},23,[199,969,970],{"class":209},"style.setLabelSettings(settings)\n",[199,972,974],{"class":201,"line":973},24,[199,975,236],{"emptyLinePlaceholder":235},[199,977,979,982,984],{"class":201,"line":978},25,[199,980,981],{"class":209},"labeling ",[199,983,245],{"class":205},[199,985,986],{"class":209}," QgsVectorTileBasicLabeling()\n",[199,988,990],{"class":201,"line":989},26,[199,991,992],{"class":209},"labeling.setStyles([style])\n",[199,994,996],{"class":201,"line":995},27,[199,997,998],{"class":209},"layer.setLabeling(labeling)\n",[199,1000,1002],{"class":201,"line":1001},28,[199,1003,591],{"class":209},[14,1005,1006,1008,1009,1013,1014,1017,1018,1021,1022,1025],{},[167,1007,390],{}," Vector tile labelling uses its own classes rather than the vector-layer ones, because a label style has to say ",[1010,1011,1012],"em",{},"which sub-layer"," it applies to and at which zooms — hence ",[185,1015,1016],{},"setLayerName(\"place\")"," and the zoom range. The ",[185,1019,1020],{},"coalesce()"," expression picks a Welsh name where the tiles carry one and falls back to the default, which is the pattern for any multilingual source and impossible with a raster basemap. ",[185,1023,1024],{},"setGeometryType()"," must match what that sub-layer holds, and a mismatch produces no labels with no complaint.",[14,1027,1028,1029,1032,1033,1037],{},"Because the settings object is the ordinary ",[185,1030,1031],{},"QgsPalLayerSettings",", everything from ",[21,1034,1036],{"href":1035},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","label placement and collision handling"," applies unchanged — buffers, callouts, obstacle weights and priority all work exactly as they do on a normal layer.",[156,1039,1041],{"id":1040},"what-is-actually-in-the-tiles","What is actually in the tiles",[14,1043,1044],{},"Tile contents vary by source and by zoom, and there is no schema to consult. Asking the layer is the only reliable route.",[190,1046,1048],{"className":192,"code":1047,"language":194,"meta":195,"style":195},"from qgis.core import QgsCoordinateTransform, QgsProject\n\nrenderer = layer.renderer()\nif hasattr(renderer, \"styles\"):\n    for style in renderer.styles():\n        print(style.layerName(), style.filterExpression(), style.minZoomLevel(), style.maxZoomLevel())\n",[185,1049,1050,1061,1065,1075,1091,1105],{"__ignoreMap":195},[199,1051,1052,1054,1056,1058],{"class":201,"line":202},[199,1053,206],{"class":205},[199,1055,210],{"class":209},[199,1057,213],{"class":205},[199,1059,1060],{"class":209}," QgsCoordinateTransform, QgsProject\n",[199,1062,1063],{"class":201,"line":219},[199,1064,236],{"emptyLinePlaceholder":235},[199,1066,1067,1070,1072],{"class":201,"line":232},[199,1068,1069],{"class":209},"renderer ",[199,1071,245],{"class":205},[199,1073,1074],{"class":209}," layer.renderer()\n",[199,1076,1077,1079,1082,1085,1088],{"class":201,"line":239},[199,1078,356],{"class":205},[199,1080,1081],{"class":252}," hasattr",[199,1083,1084],{"class":209},"(renderer, ",[199,1086,1087],{"class":248},"\"styles\"",[199,1089,1090],{"class":209},"):\n",[199,1092,1093,1096,1099,1102],{"class":201,"line":270},[199,1094,1095],{"class":205},"    for",[199,1097,1098],{"class":209}," style ",[199,1100,1101],{"class":205},"in",[199,1103,1104],{"class":209}," renderer.styles():\n",[199,1106,1107,1110],{"class":201,"line":281},[199,1108,1109],{"class":252},"        print",[199,1111,1112],{"class":209},"(style.layerName(), style.filterExpression(), style.minZoomLevel(), style.maxZoomLevel())\n",[14,1114,1115,1117],{},[167,1116,390],{}," After a style is applied, the renderer's styles list names each sub-layer the style knows about, along with the filter and the zoom range it applies at. That is not the same as everything the tiles contain — a style ignores sub-layers it does not draw — but it is usually the practical list. For a definitive answer, open one tile with a MapBox Vector Tile reader outside QGIS, or consult the source's published schema.",[156,1119,1121],{"id":1120},"when-vector-tiles-are-the-wrong-tool","When vector tiles are the wrong tool",[14,1123,1124],{},"Three properties of the format rule out whole classes of use.",[14,1126,1127,1128,1130,1131,1134,1135,1138],{},"Geometry is ",[167,1129,122],{},", so a building footprint at zoom 12 is not the same shape as at zoom 16 and neither is the surveyed shape. Features are ",[167,1132,1133],{},"clipped at tile boundaries",", so a single road may appear as several partial pieces with duplicated attributes — measuring its length gives an answer that depends on the tile grid. And attributes are ",[167,1136,1137],{},"whatever the tile author included",", typically a name and a class, not the source dataset's full table.",[14,1140,1141,1142,1146],{},"For analysis, get the real data: a ",[21,1143,1145],{"href":1144},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-wfs-layer-pyqgis\u002F","WFS layer"," if the service offers one, or a bulk download. Vector tiles are a basemap that happens to be restylable, and using them as one is entirely correct.",[156,1148,1150],{"id":1149},"qgis-version-compatibility","QGIS version compatibility",[14,1152,1153,1154,1156],{},"The examples target ",[167,1155,169],{}," (Python 3.12).",[1158,1159,1160,1176],"table",{},[1161,1162,1163],"thead",{},[1164,1165,1166,1170,1173],"tr",{},[1167,1168,1169],"th",{},"QGIS version",[1167,1171,1172],{},"Python",[1167,1174,1175],{},"Notes",[1177,1178,1179,1194,1204,1219,1230],"tbody",{},[1164,1180,1181,1185,1188],{},[1182,1183,1184],"td",{},"3.14",[1182,1186,1187],{},"3.7",[1182,1189,1190,1193],{},[185,1191,1192],{},"QgsVectorTileLayer"," introduced; XYZ and MBTiles sources supported.",[1164,1195,1196,1199,1201],{},[1182,1197,1198],{},"3.16 LTR",[1182,1200,1187],{},[1182,1202,1203],{},"MapBox GL style JSON conversion added.",[1164,1205,1206,1209,1212],{},[1182,1207,1208],{},"3.22 LTR",[1182,1210,1211],{},"3.9",[1182,1213,1214,1215,1218],{},"Labelling from tile attributes improved; ",[185,1216,1217],{},"sourceMinZoom"," exposed.",[1164,1220,1221,1224,1227],{},[1182,1222,1223],{},"3.34 LTR",[1182,1225,1226],{},"3.12",[1182,1228,1229],{},"Baseline for this page.",[1164,1231,1232,1235,1237],{},[1182,1233,1234],{},"3.40+",[1182,1236,1226],{},[1182,1238,1239],{},"Vector tile writing supported, so QGIS can generate tiles as well as read them.",[156,1241,1243],{"id":1242},"troubleshooting","Troubleshooting",[161,1245,1246,1252,1262,1268,1274,1280],{},[164,1247,1248,1251],{},[167,1249,1250],{},"The layer is valid but nothing draws."," The URL was not percent-encoded, or the zoom range does not overlap the source's.",[164,1253,1254,596,1257,256,1259,1261],{},[167,1255,1256],{},"Tiles load at some zooms and not others.",[185,1258,414],{},[185,1260,417],{}," are wrong. Read them from the source metadata.",[164,1263,1264,1267],{},[167,1265,1266],{},"Everything is grey rectangles."," No style was applied and the default renderer is drawing raw geometry. Load a style.",[164,1269,1270,1273],{},[167,1271,1272],{},"The style loaded but looks wrong."," MapBox GL features that QGIS cannot represent were dropped in conversion. Adjust the resulting rules directly.",[164,1275,1276,1279],{},[167,1277,1278],{},"Measuring a feature gives the wrong length."," Features are clipped at tile boundaries. Use real vector data for measurement.",[164,1281,1282,1285,1286,1290],{},[167,1283,1284],{},"The API key is visible in the project file."," It is stored in the URI. Use the ",[21,1287,1289],{"href":1288},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fstore-credentials-with-qgsauthmanager-pyqgis\u002F","authentication manager"," to keep it out.",[156,1292,1294],{"id":1293},"conclusion","Conclusion",[14,1296,1297],{},"Percent-encode the template URL, match the zoom range to the source, load a style before judging the result, and treat the layer as a basemap rather than as data. When measurement or attributes matter, fetch the real dataset instead — the tiles were generalised and clipped on purpose.",[156,1299,1301],{"id":1300},"frequently-asked-questions","Frequently Asked Questions",[14,1303,1304,1307],{},[167,1305,1306],{},"Can I query a feature by clicking it?","\nIdentify works and returns whatever attributes the tile carries, which is usually a name and a class. It is not the source record, and the feature may be one clipped piece of a longer one.",[14,1309,1310,1313],{},[167,1311,1312],{},"Does QGIS cache tiles?","\nYes, through the network cache, with size and expiry configurable in the settings. For genuinely offline work, download an MBTiles file rather than relying on the cache.",[14,1315,1316,1319],{},[167,1317,1318],{},"Can I export vector tiles to a file layer?","\nQGIS 3.40 and newer can write vector tiles, and converting a tile source to a vector layer is possible for a small extent — but the result inherits the generalisation and clipping, so it is a poor substitute for the source data.",[14,1321,1322,1325,1326,1329],{},[167,1323,1324],{},"How do I keep the API key out of the project file?","\nStore the credential in the ",[21,1327,1328],{"href":1288},"authentication database"," and reference its id in the URI, so the project holds a reference rather than the secret.",[156,1331,1333],{"id":1332},"related","Related",[161,1335,1336,1341,1347,1352,1358],{},[164,1337,1338,1340],{},[21,1339,24],{"href":23}," — the guide this recipe belongs to",[164,1342,1343],{},[21,1344,1346],{"href":1345},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fadd-xyz-tile-basemap-pyqgis\u002F","Add an XYZ Tile Basemap in PyQGIS",[164,1348,1349],{},[21,1350,1351],{"href":1144},"Load a WFS Layer in PyQGIS",[164,1353,1354],{},[21,1355,1357],{"href":1356},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fread-cloud-optimized-geotiff-pyqgis\u002F","Read a Cloud Optimized GeoTIFF in PyQGIS",[164,1359,1360],{},[21,1361,1362],{"href":1288},"Store Credentials with QgsAuthManager in PyQGIS",[1364,1365,1366],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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":195,"searchDepth":219,"depth":219,"links":1368},[1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380],{"id":158,"depth":219,"text":159},{"id":179,"depth":219,"text":180},{"id":427,"depth":219,"text":428},{"id":510,"depth":219,"text":511},{"id":748,"depth":219,"text":749},{"id":1040,"depth":219,"text":1041},{"id":1120,"depth":219,"text":1121},{"id":1149,"depth":219,"text":1150},{"id":1242,"depth":219,"text":1243},{"id":1293,"depth":219,"text":1294},{"id":1300,"depth":219,"text":1301},{"id":1332,"depth":219,"text":1333},"Add XYZ and MBTiles vector tile sources from Python with QgsVectorTileLayer — the URI parameters, zoom ranges, applying a style JSON, and why attributes are not queryable the usual way.","md",{"slug":1384,"type":1385,"breadcrumb":1386,"datePublished":1387,"dateModified":1387},"load-vector-tile-layer-pyqgis","article","Vector Tiles","2026-08-27","\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-vector-tile-layer-pyqgis",{"title":5,"description":1381},"spatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-vector-tile-layer-pyqgis\u002Findex","0pcesBK0KySwZy3dB_7Rodk_HTAEKGlIzAtPeZvdoJA",1787823365050]