[{"data":1,"prerenderedAt":1590},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Finstall-proj-datum-grids-pyqgis":3},{"id":4,"title":5,"body":6,"description":1579,"extension":1580,"meta":1581,"navigation":226,"path":1586,"seo":1587,"stem":1588,"__hash__":1589},"docs\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Finstall-proj-datum-grids-pyqgis\u002Findex.md","Install PROJ Datum Grids for PyQGIS",{"type":7,"value":8,"toc":1565},"minimark",[9,13,17,26,168,173,186,190,193,481,488,492,495,569,674,692,696,699,1078,1094,1098,1101,1190,1195,1199,1202,1265,1317,1333,1337,1340,1414,1419,1423,1451,1455,1484,1488,1494,1498,1504,1510,1516,1522,1528,1532,1561],[10,11,5],"h1",{"id":12},"install-proj-datum-grids-for-pyqgis",[14,15,16],"p",{},"Transforming coordinates between datums — an old national datum and ETRS89, NAD27 and NAD83, a geoid model for heights — is only as accurate as the method PROJ can use. The most accurate methods interpolate in grid files published by mapping agencies; without the grid, PROJ falls back to a simple Helmert shift that can be off by one to several metres. QGIS installs ship only a few grids, so whether a transformation is accurate to centimetres or to metres often depends on files nobody remembered to install.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems",". It finds which grid an operation needs, checks availability from Python, installs grids into PROJ's user directory, enables PROJ's content delivery network as an alternative, and makes scripts, servers and containers use the same grids so results agree everywhere.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,104,108,116,123,128,132,137,141,145,149,155,160,164],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 236","img","A datum transformation using a grid file for centimetre accuracy, or silently falling back to a Helmert shift accurate only to metres when the grid is missing","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Grid or fallback",[40,41,42],"desc",{},"A transformation from an old national datum to ETRS89 has a grid-based operation accurate to a few centimetres, which needs a grid file such as a NTv2 file, and a Helmert fallback accurate to about one to three metres. If the grid is missing, PROJ silently uses the fallback; QGIS warns in the interface but scripts get no message. Installing the grid makes the accurate operation available.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","236","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gridWhyArrow","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-family:sans-serif;font-weight:bold","#17211d","middle","Same call, very different accuracy",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","70","200","120","#fffdf7","#59645f","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":73,"textAnchor":74},"124","121.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","transform",[68,92,95],{"x":87,"y":93,"style":94,"fill":83,"textAnchor":74},"145.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","old datum → ETRS89",[97,98],"line",{"x1":99,"y1":100,"x2":101,"y2":102,"stroke":66,"style":103},"224","100","286","80","stroke-width:1.8;marker-end:url(#gridWhyArrow)",[97,105],{"x1":99,"y1":106,"x2":101,"y2":107,"stroke":66,"style":103},"160","180",[44,109],{"x":110,"y":111,"width":112,"height":113,"rx":58,"fill":114,"stroke":115,"style":84},"290","46","250","74","#e8efe6","#15803d",[68,117,122],{"x":118,"y":119,"style":120,"fill":121,"textAnchor":74},"415","66.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","#166534","grid file present",[68,124,127],{"x":118,"y":125,"style":126,"fill":83,"textAnchor":74},"86.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","NTv2 \u002F GeoTIFF grid",[68,129,131],{"x":118,"y":130,"style":126,"fill":83,"textAnchor":74},"106.6","± a few cm",[44,133],{"x":110,"y":134,"width":112,"height":113,"rx":58,"fill":135,"stroke":136,"style":84},"146","#fdf2e2","#b91c1c",[68,138,140],{"x":118,"y":139,"style":120,"fill":136,"textAnchor":74},"166.6","grid missing",[68,142,144],{"x":118,"y":143,"style":126,"fill":83,"textAnchor":74},"186.6","Helmert fallback",[68,146,148],{"x":118,"y":147,"style":126,"fill":83,"textAnchor":74},"206.6","± 1–3 m, silently",[44,150],{"x":151,"y":152,"width":153,"height":152,"rx":58,"fill":135,"stroke":154,"style":84},"572","90","164","#b45309",[68,156,159],{"x":157,"y":158,"style":89,"fill":154,"textAnchor":74},"654","116.78","scripts",[68,161,163],{"x":157,"y":162,"style":94,"fill":83,"textAnchor":74},"138.78","no warning",[68,165,167],{"x":157,"y":166,"style":94,"fill":83,"textAnchor":74},"160.78","check first",[169,170,172],"h2",{"id":171},"prerequisites","Prerequisites",[174,175,176,180,183],"ul",{},[177,178,179],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, which use PROJ 9 with grids in GeoTIFF format.",[177,181,182],{},"Permission to write to your user profile, or to the PROJ data directory on servers.",[177,184,185],{},"The pair of CRSs you transform between.",[169,187,189],{"id":188},"find-which-grid-an-operation-needs","Find which grid an operation needs",[14,191,192],{},"QGIS exposes PROJ's list of candidate operations for any CRS pair, with each operation's accuracy, availability and the grids it needs.",[194,195,200],"pre",{"className":196,"code":197,"language":198,"meta":199,"style":199},"language-python shiki shiki-themes github-dark","from qgis.core import QgsCoordinateReferenceSystem, QgsDatumTransform\n\nsrc = QgsCoordinateReferenceSystem(\"EPSG:31467\")   # DHDN \u002F 3-degree Gauss-Kruger zone 3\ndst = QgsCoordinateReferenceSystem(\"EPSG:25832\")   # ETRS89 \u002F UTM zone 32N\n\nfor op in QgsDatumTransform.operations(src, dst):\n    acc = f\"±{op.accuracy} m\" if op.accuracy >= 0 else \"unknown\"\n    state = \"available\" if op.isAvailable else \"MISSING\"\n    print(f\"{state:\u003C9} {acc:\u003C10} {op.name}\")\n    for g in op.grids:\n        print(f\"           grid {g.shortName} ({'installed' if g.isAvailable else 'not installed'})\"\n              f\"  url={g.url}\")\n","python","",[201,202,203,221,228,251,269,274,289,334,356,404,418,461],"code",{"__ignoreMap":199},[204,205,207,211,215,218],"span",{"class":97,"line":206},1,[204,208,210],{"class":209},"snl16","from",[204,212,214],{"class":213},"s95oV"," qgis.core ",[204,216,217],{"class":209},"import",[204,219,220],{"class":213}," QgsCoordinateReferenceSystem, QgsDatumTransform\n",[204,222,224],{"class":97,"line":223},2,[204,225,227],{"emptyLinePlaceholder":226},true,"\n",[204,229,231,234,237,240,244,247],{"class":97,"line":230},3,[204,232,233],{"class":213},"src ",[204,235,236],{"class":209},"=",[204,238,239],{"class":213}," QgsCoordinateReferenceSystem(",[204,241,243],{"class":242},"sU2Wk","\"EPSG:31467\"",[204,245,246],{"class":213},")   ",[204,248,250],{"class":249},"sjoCn","# DHDN \u002F 3-degree Gauss-Kruger zone 3\n",[204,252,254,257,259,261,264,266],{"class":97,"line":253},4,[204,255,256],{"class":213},"dst ",[204,258,236],{"class":209},[204,260,239],{"class":213},[204,262,263],{"class":242},"\"EPSG:25832\"",[204,265,246],{"class":213},[204,267,268],{"class":249},"# ETRS89 \u002F UTM zone 32N\n",[204,270,272],{"class":97,"line":271},5,[204,273,227],{"emptyLinePlaceholder":226},[204,275,277,280,283,286],{"class":97,"line":276},6,[204,278,279],{"class":209},"for",[204,281,282],{"class":213}," op ",[204,284,285],{"class":209},"in",[204,287,288],{"class":213}," QgsDatumTransform.operations(src, dst):\n",[204,290,292,295,297,300,303,307,310,313,316,319,322,325,328,331],{"class":97,"line":291},7,[204,293,294],{"class":213},"    acc ",[204,296,236],{"class":209},[204,298,299],{"class":209}," f",[204,301,302],{"class":242},"\"±",[204,304,306],{"class":305},"sDLfK","{",[204,308,309],{"class":213},"op.accuracy",[204,311,312],{"class":305},"}",[204,314,315],{"class":242}," m\"",[204,317,318],{"class":209}," if",[204,320,321],{"class":213}," op.accuracy ",[204,323,324],{"class":209},">=",[204,326,327],{"class":305}," 0",[204,329,330],{"class":209}," else",[204,332,333],{"class":242}," \"unknown\"\n",[204,335,337,340,342,345,347,350,353],{"class":97,"line":336},8,[204,338,339],{"class":213},"    state ",[204,341,236],{"class":209},[204,343,344],{"class":242}," \"available\"",[204,346,318],{"class":209},[204,348,349],{"class":213}," op.isAvailable ",[204,351,352],{"class":209},"else",[204,354,355],{"class":242}," \"MISSING\"\n",[204,357,359,362,365,368,371,373,376,379,381,384,387,390,392,394,397,399,401],{"class":97,"line":358},9,[204,360,361],{"class":305},"    print",[204,363,364],{"class":213},"(",[204,366,367],{"class":209},"f",[204,369,370],{"class":242},"\"",[204,372,306],{"class":305},[204,374,375],{"class":213},"state",[204,377,378],{"class":209},":\u003C9",[204,380,312],{"class":305},[204,382,383],{"class":305}," {",[204,385,386],{"class":213},"acc",[204,388,389],{"class":209},":\u003C10",[204,391,312],{"class":305},[204,393,383],{"class":305},[204,395,396],{"class":213},"op.name",[204,398,312],{"class":305},[204,400,370],{"class":242},[204,402,403],{"class":213},")\n",[204,405,407,410,413,415],{"class":97,"line":406},10,[204,408,409],{"class":209},"    for",[204,411,412],{"class":213}," g ",[204,414,285],{"class":209},[204,416,417],{"class":213}," op.grids:\n",[204,419,421,424,426,428,431,433,436,438,441,443,446,448,451,453,456,458],{"class":97,"line":420},11,[204,422,423],{"class":305},"        print",[204,425,364],{"class":213},[204,427,367],{"class":209},[204,429,430],{"class":242},"\"           grid ",[204,432,306],{"class":305},[204,434,435],{"class":213},"g.shortName",[204,437,312],{"class":305},[204,439,440],{"class":242}," (",[204,442,306],{"class":305},[204,444,445],{"class":242},"'installed'",[204,447,318],{"class":209},[204,449,450],{"class":213}," g.isAvailable ",[204,452,352],{"class":209},[204,454,455],{"class":242}," 'not installed'",[204,457,312],{"class":305},[204,459,460],{"class":242},")\"\n",[204,462,464,467,470,472,475,477,479],{"class":97,"line":463},12,[204,465,466],{"class":209},"              f",[204,468,469],{"class":242},"\"  url=",[204,471,306],{"class":305},[204,473,474],{"class":213},"g.url",[204,476,312],{"class":305},[204,478,370],{"class":242},[204,480,403],{"class":213},[14,482,483,487],{},[484,485,486],"strong",{},"Breakdown:"," Operations are listed from most to least accurate. Each grid entry names the file, whether it is installed, and the URL it can be downloaded from — PROJ's CDN or the publishing agency. An operation marked missing with a grid \"not installed\" is exactly the case where installing a file upgrades the transformation from metres to centimetres. Running this for every CRS pair a project uses is a quick audit.",[169,489,491],{"id":490},"check-what-a-transform-actually-uses","Check what a transform actually uses",[14,493,494],{},"Knowing that a grid exists is not enough; scripts should confirm which operation a transform will use, because the fallback is silent.",[14,496,497],{},[29,498,501,504,507,510,517,520,524,529,533,538,541,546,550,554,558,562,566],{"viewBox":499,"role":32,"ariaLabel":500,"xmlns":34},"0 0 760 196","A script checking the operation a transform will use and stopping if it would fall back to a less accurate method",[36,502,503],{},"Confirm before you transform",[40,505,506],{},"A script builds a QgsCoordinateTransform with the project's transform context, then asks which coordinate operation it will use and whether the transform is falling back to a less accurate one. If the operation is not the expected grid-based one, the script stops with a message naming the missing grid rather than producing metre-level errors.",[44,508],{"x":46,"y":46,"width":47,"height":509,"fill":49},"196",[51,511,512],{},[54,513,515],{"id":514,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gridCheckArrow",[63,516],{"d":65,"fill":66},[68,518,519],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Fail loudly instead of drifting quietly",[44,521],{"x":78,"y":522,"width":523,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"60","222",[68,525,528],{"x":526,"y":527,"style":89,"fill":73,"textAnchor":74},"135","110.78","build transform",[68,530,532],{"x":526,"y":531,"style":94,"fill":83,"textAnchor":74},"136.78","with context",[97,534],{"x1":535,"y1":81,"x2":536,"y2":81,"stroke":66,"style":537},"246","288","stroke-width:1.8;marker-end:url(#gridCheckArrow)",[44,539],{"x":540,"y":522,"width":80,"height":81,"rx":58,"fill":135,"stroke":154,"style":84},"292",[68,542,545],{"x":543,"y":544,"style":89,"fill":154,"textAnchor":74},"392","97.78","which operation?",[68,547,549],{"x":543,"y":548,"style":94,"fill":83,"textAnchor":74},"123.78","accuracy?",[68,551,553],{"x":543,"y":552,"style":94,"fill":83,"textAnchor":74},"149.78","fallback?",[97,555],{"x1":556,"y1":81,"x2":557,"y2":81,"stroke":66,"style":537},"492","534",[44,559],{"x":560,"y":522,"width":561,"height":81,"rx":58,"fill":114,"stroke":115,"style":84},"538","198",[68,563,565],{"x":564,"y":527,"style":89,"fill":121,"textAnchor":74},"637","expected → go",[68,567,568],{"x":564,"y":531,"style":94,"fill":83,"textAnchor":74},"else stop",[194,570,572],{"className":196,"code":571,"language":198,"meta":199,"style":199},"from qgis.core import QgsCoordinateTransform, QgsProject, QgsPointXY\n\nxf = QgsCoordinateTransform(src, dst, QgsProject.instance())\np = xf.transform(QgsPointXY(3_480_000, 5_420_000))\nprint(\"operation:\", xf.instantiatedCoordinateOperationDetails().name)\nprint(\"accuracy:\", xf.instantiatedCoordinateOperationDetails().accuracy, \"m\")\nif xf.fallbackOperationOccurred():\n    raise RuntimeError(\"PROJ fell back to a less accurate operation - install the grid\")\n",[201,573,574,585,589,599,621,634,651,659],{"__ignoreMap":199},[204,575,576,578,580,582],{"class":97,"line":206},[204,577,210],{"class":209},[204,579,214],{"class":213},[204,581,217],{"class":209},[204,583,584],{"class":213}," QgsCoordinateTransform, QgsProject, QgsPointXY\n",[204,586,587],{"class":97,"line":223},[204,588,227],{"emptyLinePlaceholder":226},[204,590,591,594,596],{"class":97,"line":230},[204,592,593],{"class":213},"xf ",[204,595,236],{"class":209},[204,597,598],{"class":213}," QgsCoordinateTransform(src, dst, QgsProject.instance())\n",[204,600,601,604,606,609,612,615,618],{"class":97,"line":253},[204,602,603],{"class":213},"p ",[204,605,236],{"class":209},[204,607,608],{"class":213}," xf.transform(QgsPointXY(",[204,610,611],{"class":305},"3_480_000",[204,613,614],{"class":213},", ",[204,616,617],{"class":305},"5_420_000",[204,619,620],{"class":213},"))\n",[204,622,623,626,628,631],{"class":97,"line":271},[204,624,625],{"class":305},"print",[204,627,364],{"class":213},[204,629,630],{"class":242},"\"operation:\"",[204,632,633],{"class":213},", xf.instantiatedCoordinateOperationDetails().name)\n",[204,635,636,638,640,643,646,649],{"class":97,"line":276},[204,637,625],{"class":305},[204,639,364],{"class":213},[204,641,642],{"class":242},"\"accuracy:\"",[204,644,645],{"class":213},", xf.instantiatedCoordinateOperationDetails().accuracy, ",[204,647,648],{"class":242},"\"m\"",[204,650,403],{"class":213},[204,652,653,656],{"class":97,"line":291},[204,654,655],{"class":209},"if",[204,657,658],{"class":213}," xf.fallbackOperationOccurred():\n",[204,660,661,664,667,669,672],{"class":97,"line":336},[204,662,663],{"class":209},"    raise",[204,665,666],{"class":305}," RuntimeError",[204,668,364],{"class":213},[204,670,671],{"class":242},"\"PROJ fell back to a less accurate operation - install the grid\"",[204,673,403],{"class":213},[14,675,676,678,679,682,683,686,687,691],{},[484,677,486],{}," ",[201,680,681],{},"instantiatedCoordinateOperationDetails()"," reports the operation actually used after a transformation has run. ",[201,684,685],{},"fallbackOperationOccurred()"," is true when the preferred operation could not be used — typically because its grid was missing — and PROJ substituted another. Raising an error in that case turns a silent metre-level error into a clear instruction. Run a transform once before a batch and check, as in ",[21,688,690],{"href":689},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fbatch-reproject-vector-layers-pyqgis\u002F","batch reprojecting vector layers",".",[169,693,695],{"id":694},"install-grids-into-the-user-directory","Install grids into the user directory",[14,697,698],{},"PROJ searches several directories for grids, including a per-user directory that needs no administrator rights. Downloading the grid file there makes it available to QGIS and to every PROJ-based tool run by that user.",[194,700,702],{"className":196,"code":701,"language":198,"meta":199,"style":199},"import os\nfrom pathlib import Path\nfrom qgis.core import QgsApplication, QgsNetworkContentFetcher\nfrom qgis.PyQt.QtCore import QUrl, QEventLoop\n\ndef proj_user_dir():\n    from qgis.core import QgsProjUtils\n    for d in QgsProjUtils.searchPaths():\n        if \"proj\" in d.lower() and os.access(d, os.W_OK):\n            return d\n    return os.path.join(QgsApplication.qgisSettingsDirPath(), \"proj\")\n\ntarget_dir = Path(proj_user_dir())\ntarget_dir.mkdir(parents=True, exist_ok=True)\nprint(\"installing into\", target_dir)\n\ndef download(url, dest):\n    fetcher = QgsNetworkContentFetcher()\n    loop = QEventLoop()\n    fetcher.finished.connect(loop.quit)\n    fetcher.fetchContent(QUrl(url))\n    loop.exec_()\n    data = fetcher.reply().readAll()\n    Path(dest).write_bytes(bytes(data))\n    return len(data)\n\nfor op in QgsDatumTransform.operations(src, dst):\n    for g in op.grids:\n        if not g.isAvailable and g.url:\n            size = download(g.url, target_dir \u002F g.shortName)\n            print(f\"downloaded {g.shortName}: {size \u002F 1e6:.1f} MB\")\n",[201,703,704,711,723,734,746,750,762,774,786,812,820,833,837,848,874,887,892,903,914,925,931,937,943,954,966,977,982,993,1004,1019,1036],{"__ignoreMap":199},[204,705,706,708],{"class":97,"line":206},[204,707,217],{"class":209},[204,709,710],{"class":213}," os\n",[204,712,713,715,718,720],{"class":97,"line":223},[204,714,210],{"class":209},[204,716,717],{"class":213}," pathlib ",[204,719,217],{"class":209},[204,721,722],{"class":213}," Path\n",[204,724,725,727,729,731],{"class":97,"line":230},[204,726,210],{"class":209},[204,728,214],{"class":213},[204,730,217],{"class":209},[204,732,733],{"class":213}," QgsApplication, QgsNetworkContentFetcher\n",[204,735,736,738,741,743],{"class":97,"line":253},[204,737,210],{"class":209},[204,739,740],{"class":213}," qgis.PyQt.QtCore ",[204,742,217],{"class":209},[204,744,745],{"class":213}," QUrl, QEventLoop\n",[204,747,748],{"class":97,"line":271},[204,749,227],{"emptyLinePlaceholder":226},[204,751,752,755,759],{"class":97,"line":276},[204,753,754],{"class":209},"def",[204,756,758],{"class":757},"svObZ"," proj_user_dir",[204,760,761],{"class":213},"():\n",[204,763,764,767,769,771],{"class":97,"line":291},[204,765,766],{"class":209},"    from",[204,768,214],{"class":213},[204,770,217],{"class":209},[204,772,773],{"class":213}," QgsProjUtils\n",[204,775,776,778,781,783],{"class":97,"line":336},[204,777,409],{"class":209},[204,779,780],{"class":213}," d ",[204,782,285],{"class":209},[204,784,785],{"class":213}," QgsProjUtils.searchPaths():\n",[204,787,788,791,794,797,800,803,806,809],{"class":97,"line":358},[204,789,790],{"class":209},"        if",[204,792,793],{"class":242}," \"proj\"",[204,795,796],{"class":209}," in",[204,798,799],{"class":213}," d.lower() ",[204,801,802],{"class":209},"and",[204,804,805],{"class":213}," os.access(d, os.",[204,807,808],{"class":305},"W_OK",[204,810,811],{"class":213},"):\n",[204,813,814,817],{"class":97,"line":406},[204,815,816],{"class":209},"            return",[204,818,819],{"class":213}," d\n",[204,821,822,825,828,831],{"class":97,"line":420},[204,823,824],{"class":209},"    return",[204,826,827],{"class":213}," os.path.join(QgsApplication.qgisSettingsDirPath(), ",[204,829,830],{"class":242},"\"proj\"",[204,832,403],{"class":213},[204,834,835],{"class":97,"line":463},[204,836,227],{"emptyLinePlaceholder":226},[204,838,840,843,845],{"class":97,"line":839},13,[204,841,842],{"class":213},"target_dir ",[204,844,236],{"class":209},[204,846,847],{"class":213}," Path(proj_user_dir())\n",[204,849,851,854,858,860,863,865,868,870,872],{"class":97,"line":850},14,[204,852,853],{"class":213},"target_dir.mkdir(",[204,855,857],{"class":856},"s9osk","parents",[204,859,236],{"class":209},[204,861,862],{"class":305},"True",[204,864,614],{"class":213},[204,866,867],{"class":856},"exist_ok",[204,869,236],{"class":209},[204,871,862],{"class":305},[204,873,403],{"class":213},[204,875,877,879,881,884],{"class":97,"line":876},15,[204,878,625],{"class":305},[204,880,364],{"class":213},[204,882,883],{"class":242},"\"installing into\"",[204,885,886],{"class":213},", target_dir)\n",[204,888,890],{"class":97,"line":889},16,[204,891,227],{"emptyLinePlaceholder":226},[204,893,895,897,900],{"class":97,"line":894},17,[204,896,754],{"class":209},[204,898,899],{"class":757}," download",[204,901,902],{"class":213},"(url, dest):\n",[204,904,906,909,911],{"class":97,"line":905},18,[204,907,908],{"class":213},"    fetcher ",[204,910,236],{"class":209},[204,912,913],{"class":213}," QgsNetworkContentFetcher()\n",[204,915,917,920,922],{"class":97,"line":916},19,[204,918,919],{"class":213},"    loop ",[204,921,236],{"class":209},[204,923,924],{"class":213}," QEventLoop()\n",[204,926,928],{"class":97,"line":927},20,[204,929,930],{"class":213},"    fetcher.finished.connect(loop.quit)\n",[204,932,934],{"class":97,"line":933},21,[204,935,936],{"class":213},"    fetcher.fetchContent(QUrl(url))\n",[204,938,940],{"class":97,"line":939},22,[204,941,942],{"class":213},"    loop.exec_()\n",[204,944,946,949,951],{"class":97,"line":945},23,[204,947,948],{"class":213},"    data ",[204,950,236],{"class":209},[204,952,953],{"class":213}," fetcher.reply().readAll()\n",[204,955,957,960,963],{"class":97,"line":956},24,[204,958,959],{"class":213},"    Path(dest).write_bytes(",[204,961,962],{"class":305},"bytes",[204,964,965],{"class":213},"(data))\n",[204,967,969,971,974],{"class":97,"line":968},25,[204,970,824],{"class":209},[204,972,973],{"class":305}," len",[204,975,976],{"class":213},"(data)\n",[204,978,980],{"class":97,"line":979},26,[204,981,227],{"emptyLinePlaceholder":226},[204,983,985,987,989,991],{"class":97,"line":984},27,[204,986,279],{"class":209},[204,988,282],{"class":213},[204,990,285],{"class":209},[204,992,288],{"class":213},[204,994,996,998,1000,1002],{"class":97,"line":995},28,[204,997,409],{"class":209},[204,999,412],{"class":213},[204,1001,285],{"class":209},[204,1003,417],{"class":213},[204,1005,1007,1009,1012,1014,1016],{"class":97,"line":1006},29,[204,1008,790],{"class":209},[204,1010,1011],{"class":209}," not",[204,1013,450],{"class":213},[204,1015,802],{"class":209},[204,1017,1018],{"class":213}," g.url:\n",[204,1020,1022,1025,1027,1030,1033],{"class":97,"line":1021},30,[204,1023,1024],{"class":213},"            size ",[204,1026,236],{"class":209},[204,1028,1029],{"class":213}," download(g.url, target_dir ",[204,1031,1032],{"class":209},"\u002F",[204,1034,1035],{"class":213}," g.shortName)\n",[204,1037,1039,1042,1044,1046,1049,1051,1053,1055,1058,1060,1063,1065,1068,1071,1073,1076],{"class":97,"line":1038},31,[204,1040,1041],{"class":305},"            print",[204,1043,364],{"class":213},[204,1045,367],{"class":209},[204,1047,1048],{"class":242},"\"downloaded ",[204,1050,306],{"class":305},[204,1052,435],{"class":213},[204,1054,312],{"class":305},[204,1056,1057],{"class":242},": ",[204,1059,306],{"class":305},[204,1061,1062],{"class":213},"size ",[204,1064,1032],{"class":209},[204,1066,1067],{"class":305}," 1e6",[204,1069,1070],{"class":209},":.1f",[204,1072,312],{"class":305},[204,1074,1075],{"class":242}," MB\"",[204,1077,403],{"class":213},[14,1079,1080,678,1082,1085,1086,1089,1090,1093],{},[484,1081,486],{},[201,1083,1084],{},"QgsProjUtils.searchPaths()"," lists the directories PROJ will search; the first writable one that looks like a PROJ directory is a good target, with QGIS's own profile ",[201,1087,1088],{},"proj"," folder as fallback — QGIS adds it to PROJ's search path. Downloading through QGIS's network stack respects proxy and authentication settings, which a plain ",[201,1091,1092],{},"urllib"," call might not. Restart QGIS after installing so PROJ re-reads its grid list; then rerun the operations check to confirm the grid is now available.",[169,1095,1097],{"id":1096},"or-enable-projs-network-access","Or enable PROJ's network access",[14,1099,1100],{},"Instead of installing grids, PROJ can fetch the parts of grids it needs from its CDN on demand and cache them locally. QGIS exposes this as a setting.",[194,1102,1104],{"className":196,"code":1103,"language":198,"meta":199,"style":199},"from qgis.core import QgsSettings\n\ns = QgsSettings()\nprint(\"network grids enabled:\", s.value(\"proj\u002Fproj_network\", False, type=bool))\ns.setValue(\"proj\u002Fproj_network\", True)\ns.setValue(\"proj\u002Fproj_network_url\", \"https:\u002F\u002Fcdn.proj.org\")\n",[201,1105,1106,1117,1121,1131,1163,1176],{"__ignoreMap":199},[204,1107,1108,1110,1112,1114],{"class":97,"line":206},[204,1109,210],{"class":209},[204,1111,214],{"class":213},[204,1113,217],{"class":209},[204,1115,1116],{"class":213}," QgsSettings\n",[204,1118,1119],{"class":97,"line":223},[204,1120,227],{"emptyLinePlaceholder":226},[204,1122,1123,1126,1128],{"class":97,"line":230},[204,1124,1125],{"class":213},"s ",[204,1127,236],{"class":209},[204,1129,1130],{"class":213}," QgsSettings()\n",[204,1132,1133,1135,1137,1140,1143,1146,1148,1151,1153,1156,1158,1161],{"class":97,"line":253},[204,1134,625],{"class":305},[204,1136,364],{"class":213},[204,1138,1139],{"class":242},"\"network grids enabled:\"",[204,1141,1142],{"class":213},", s.value(",[204,1144,1145],{"class":242},"\"proj\u002Fproj_network\"",[204,1147,614],{"class":213},[204,1149,1150],{"class":305},"False",[204,1152,614],{"class":213},[204,1154,1155],{"class":856},"type",[204,1157,236],{"class":209},[204,1159,1160],{"class":305},"bool",[204,1162,620],{"class":213},[204,1164,1165,1168,1170,1172,1174],{"class":97,"line":271},[204,1166,1167],{"class":213},"s.setValue(",[204,1169,1145],{"class":242},[204,1171,614],{"class":213},[204,1173,862],{"class":305},[204,1175,403],{"class":213},[204,1177,1178,1180,1183,1185,1188],{"class":97,"line":276},[204,1179,1167],{"class":213},[204,1181,1182],{"class":242},"\"proj\u002Fproj_network_url\"",[204,1184,614],{"class":213},[204,1186,1187],{"class":242},"\"https:\u002F\u002Fcdn.proj.org\"",[204,1189,403],{"class":213},[14,1191,1192,1194],{},[484,1193,486],{}," With network access on, PROJ downloads only the grid tiles covering the coordinates being transformed and caches them in a local database, so the first transformation in a new area is slower and later ones are fast. It requires internet access at transformation time, which makes it unsuitable for offline field laptops and reproducible server jobs; installing the files is the better choice there. The setting takes effect after restarting QGIS. Check the exact settings keys in your QGIS's Options → CRS and Transforms dialog, where the same option appears.",[169,1196,1198],{"id":1197},"make-servers-and-containers-match","Make servers and containers match",[14,1200,1201],{},"Desktop QGIS, a scheduled script on a server, and a Docker container may all have different grid sets — and then produce different coordinates from the same code.",[14,1203,1204],{},[29,1205,1208,1211,1214,1217,1220,1224,1228,1232,1236,1239,1242,1245,1248,1251,1255,1258,1261],{"viewBox":1206,"role":32,"ariaLabel":1207,"xmlns":34},"0 0 760 230","Desktop, server and container using different grids and disagreeing, made consistent by sharing one grid directory through PROJ_DATA",[36,1209,1210],{},"Same grids everywhere",[40,1212,1213],{},"A desktop with the grid installed, a server without it and a container with network access each pick a different operation for the same transformation, producing results that differ by metres. Pointing all three at the same grid directory through the PROJ_DATA environment variable, or baking the grids into the container image, makes them agree.",[44,1215],{"x":46,"y":46,"width":47,"height":1216,"fill":49},"230",[68,1218,1219],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"One grid set, every environment",[44,1221],{"x":78,"y":1222,"width":523,"height":1223,"rx":58,"fill":82,"stroke":83,"style":84},"56","150",[68,1225,1227],{"x":526,"y":1226,"style":89,"fill":73,"textAnchor":74},"106.78","desktop",[68,1229,1231],{"x":526,"y":1230,"style":94,"fill":83,"textAnchor":74},"134.78","grid installed",[68,1233,1235],{"x":526,"y":1234,"style":94,"fill":83,"textAnchor":74},"162.78","cm",[44,1237],{"x":1238,"y":1222,"width":523,"height":1223,"rx":58,"fill":135,"stroke":136,"style":84},"269",[68,1240,1241],{"x":70,"y":1226,"style":89,"fill":136,"textAnchor":74},"server",[68,1243,1244],{"x":70,"y":1230,"style":94,"fill":83,"textAnchor":74},"no grid",[68,1246,1247],{"x":70,"y":1234,"style":94,"fill":83,"textAnchor":74},"metres",[44,1249],{"x":1250,"y":1222,"width":523,"height":1223,"rx":58,"fill":135,"stroke":154,"style":84},"514",[68,1252,1254],{"x":1253,"y":1226,"style":89,"fill":154,"textAnchor":74},"625","container",[68,1256,1257],{"x":1253,"y":1230,"style":94,"fill":83,"textAnchor":74},"CDN, if online",[68,1259,1260],{"x":1253,"y":1234,"style":94,"fill":83,"textAnchor":74},"varies",[68,1262,1264],{"x":70,"y":1263,"style":94,"fill":83,"textAnchor":74},"226","fix: PROJ_DATA → shared grid folder, or grids in the image",[194,1266,1270],{"className":1267,"code":1268,"language":1269,"meta":199,"style":199},"language-bash shiki shiki-themes github-dark","# server or container: point PROJ at a directory with the grids\nexport PROJ_DATA=\u002Fopt\u002Fproj-data:\u002Fusr\u002Fshare\u002Fproj\npython3 \u002Fopt\u002Fscripts\u002Freproject_delivery.py\n\n# Dockerfile: bake grids into the image so every run is identical\n# COPY proj-grids\u002F \u002Fopt\u002Fproj-data\u002F\n# ENV PROJ_DATA=\u002Fopt\u002Fproj-data:\u002Fusr\u002Fshare\u002Fproj\n","bash",[201,1271,1272,1277,1290,1298,1302,1307,1312],{"__ignoreMap":199},[204,1273,1274],{"class":97,"line":206},[204,1275,1276],{"class":249},"# server or container: point PROJ at a directory with the grids\n",[204,1278,1279,1282,1285,1287],{"class":97,"line":223},[204,1280,1281],{"class":209},"export",[204,1283,1284],{"class":213}," PROJ_DATA",[204,1286,236],{"class":209},[204,1288,1289],{"class":213},"\u002Fopt\u002Fproj-data:\u002Fusr\u002Fshare\u002Fproj\n",[204,1291,1292,1295],{"class":97,"line":230},[204,1293,1294],{"class":757},"python3",[204,1296,1297],{"class":242}," \u002Fopt\u002Fscripts\u002Freproject_delivery.py\n",[204,1299,1300],{"class":97,"line":253},[204,1301,227],{"emptyLinePlaceholder":226},[204,1303,1304],{"class":97,"line":271},[204,1305,1306],{"class":249},"# Dockerfile: bake grids into the image so every run is identical\n",[204,1308,1309],{"class":97,"line":276},[204,1310,1311],{"class":249},"# COPY proj-grids\u002F \u002Fopt\u002Fproj-data\u002F\n",[204,1313,1314],{"class":97,"line":291},[204,1315,1316],{"class":249},"# ENV PROJ_DATA=\u002Fopt\u002Fproj-data:\u002Fusr\u002Fshare\u002Fproj\n",[14,1318,1319,678,1321,1324,1325,1328,1329,691],{},[484,1320,486],{},[201,1322,1323],{},"PROJ_DATA"," (called ",[201,1326,1327],{},"PROJ_LIB"," in older PROJ versions) lists directories PROJ searches, colon-separated on Linux and macOS. Adding a shared folder of grids ahead of the system directory gives every process the same set. In containers, copying grids into the image makes runs reproducible and offline-capable. Combine this with the fallback check above in every scheduled job, as described in ",[21,1330,1332],{"href":1331},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","running PyQGIS in a Docker container",[169,1334,1336],{"id":1335},"grids-for-vertical-transformations","Grids for vertical transformations",[14,1338,1339],{},"Grids are not only for horizontal datums. Converting ellipsoidal heights from GNSS into heights above a national vertical datum needs a geoid model grid, and without it the vertical component is silently left unchanged or approximated.",[194,1341,1343],{"className":196,"code":1342,"language":198,"meta":199,"style":199},"src3d = QgsCoordinateReferenceSystem(\"EPSG:4937\")      # ETRS89 3D (ellipsoidal heights)\ndst3d = QgsCoordinateReferenceSystem(\"EPSG:25832+7837\") # ETRS89 \u002F UTM 32N + DHHN2016 height\nfor op in QgsDatumTransform.operations(src3d, dst3d)[:3]:\n    print(op.isAvailable, op.name, [g.shortName for g in op.grids])\n",[201,1344,1345,1363,1381,1398],{"__ignoreMap":199},[204,1346,1347,1350,1352,1354,1357,1360],{"class":97,"line":206},[204,1348,1349],{"class":213},"src3d ",[204,1351,236],{"class":209},[204,1353,239],{"class":213},[204,1355,1356],{"class":242},"\"EPSG:4937\"",[204,1358,1359],{"class":213},")      ",[204,1361,1362],{"class":249},"# ETRS89 3D (ellipsoidal heights)\n",[204,1364,1365,1368,1370,1372,1375,1378],{"class":97,"line":223},[204,1366,1367],{"class":213},"dst3d ",[204,1369,236],{"class":209},[204,1371,239],{"class":213},[204,1373,1374],{"class":242},"\"EPSG:25832+7837\"",[204,1376,1377],{"class":213},") ",[204,1379,1380],{"class":249},"# ETRS89 \u002F UTM 32N + DHHN2016 height\n",[204,1382,1383,1385,1387,1389,1392,1395],{"class":97,"line":230},[204,1384,279],{"class":209},[204,1386,282],{"class":213},[204,1388,285],{"class":209},[204,1390,1391],{"class":213}," QgsDatumTransform.operations(src3d, dst3d)[:",[204,1393,1394],{"class":305},"3",[204,1396,1397],{"class":213},"]:\n",[204,1399,1400,1402,1405,1407,1409,1411],{"class":97,"line":253},[204,1401,361],{"class":305},[204,1403,1404],{"class":213},"(op.isAvailable, op.name, [g.shortName ",[204,1406,279],{"class":209},[204,1408,412],{"class":213},[204,1410,285],{"class":209},[204,1412,1413],{"class":213}," op.grids])\n",[14,1415,1416,1418],{},[484,1417,486],{}," A compound CRS, written as horizontal plus vertical codes, asks PROJ for operations that also convert heights. The listed grids are geoid models; installing them is what makes the height conversion real. Differences between ellipsoidal and orthometric heights are tens of metres in most places, so a missing geoid grid is not a subtle error. Check vertical results against a benchmark with a published height.",[169,1420,1422],{"id":1421},"qgis-version-compatibility","QGIS version compatibility",[14,1424,1425,1426,1429,1430,1433,1434,1437,1438,1429,1441,1444,1445,1447,1448,1450],{},"QGIS 3.34 LTR, 3.40 LTR and QGIS 4 use PROJ 9, with grids distributed as GeoTIFF files (the older NTv2 ",[201,1427,1428],{},".gsb"," and ",[201,1431,1432],{},".gtx"," formats still work). ",[201,1435,1436],{},"QgsDatumTransform.operations",", grid details, ",[201,1439,1440],{},"fallbackOperationOccurred",[201,1442,1443],{},"QgsProjUtils.searchPaths"," are available on all of these. The environment variable is ",[201,1446,1323],{}," on PROJ 9.1 and newer; ",[201,1449,1327],{}," still works as an alias.",[169,1452,1454],{"id":1453},"troubleshooting","Troubleshooting",[174,1456,1457,1463,1472,1478],{},[177,1458,1459,1462],{},[484,1460,1461],{},"The grid is installed but still \"not installed\"."," It is in a directory PROJ does not search, or QGIS has not been restarted.",[177,1464,1465,1468,1469,1471],{},[484,1466,1467],{},"Results differ between desktop and server."," Different grid sets; share ",[201,1470,1323],{}," or bake grids into the image.",[177,1473,1474,1477],{},[484,1475,1476],{},"Network grids do nothing."," The setting needs a restart, or a proxy blocks the CDN.",[177,1479,1480,1483],{},[484,1481,1482],{},"Heights are unchanged after transformation."," No geoid grid; install it and use a compound CRS.",[169,1485,1487],{"id":1486},"conclusion","Conclusion",[14,1489,1490,1491,1493],{},"List operations for each CRS pair to see which grids give the best accuracy, check ",[201,1492,1440],{}," in scripts, install missing grids into PROJ's user directory or enable network grids on connected desktops, point servers and containers at one shared grid folder, and remember that vertical transformations need geoid grids too.",[169,1495,1497],{"id":1496},"frequently-asked-questions","Frequently Asked Questions",[14,1499,1500,1503],{},[484,1501,1502],{},"Do I need grids if I only use WGS 84 and UTM?","\nUsually not; transformations within the same datum family are exact or nearly so. Grids matter for historical and national datums and for heights.",[14,1505,1506,1509],{},[484,1507,1508],{},"Where can I get all PROJ grids at once?","\nThe proj-data package from PROJ bundles all openly licensed grids; it is large, so install only what your region needs.",[14,1511,1512,1515],{},[484,1513,1514],{},"Does QGIS warn about missing grids?","\nThe desktop shows a message bar warning when a better operation is unavailable; scripts must check themselves.",[14,1517,1518,1521],{},[484,1519,1520],{},"How large are grid files?","\nMost national horizontal grids are a few megabytes; geoid models range from a few to a few hundred megabytes. Installing the handful a project needs is rarely a storage concern.",[14,1523,1524,1527],{},[484,1525,1526],{},"Are grid-based transformations always better?","\nFor their area of validity, yes. Outside it, they do not apply, and PROJ chooses another operation.",[169,1529,1531],{"id":1530},"related","Related",[174,1533,1534,1539,1545,1550,1556],{},[177,1535,1536,1538],{},[21,1537,24],{"href":23}," — the guide this recipe belongs to",[177,1540,1541],{},[21,1542,1544],{"href":1543},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis\u002F","Set Project CRS and Datum Transform in PyQGIS",[177,1546,1547],{},[21,1548,1549],{"href":689},"Batch Reproject Vector Layers in PyQGIS",[177,1551,1552],{},[21,1553,1555],{"href":1554},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates Between CRS in PyQGIS",[177,1557,1558],{},[21,1559,1560],{"href":1331},"Run PyQGIS in a Docker Container",[1562,1563,1564],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":199,"searchDepth":223,"depth":223,"links":1566},[1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578],{"id":171,"depth":223,"text":172},{"id":188,"depth":223,"text":189},{"id":490,"depth":223,"text":491},{"id":694,"depth":223,"text":695},{"id":1096,"depth":223,"text":1097},{"id":1197,"depth":223,"text":1198},{"id":1335,"depth":223,"text":1336},{"id":1421,"depth":223,"text":1422},{"id":1453,"depth":223,"text":1454},{"id":1486,"depth":223,"text":1487},{"id":1496,"depth":223,"text":1497},{"id":1530,"depth":223,"text":1531},"Get centimetre-accurate datum transformations by making PROJ's grid files available to QGIS — finding which grids an operation needs, installing them into the user PROJ directory or enabling the CDN, checking availability from Python, and making headless servers and containers behave the same.","md",{"slug":1582,"type":1583,"breadcrumb":1584,"datePublished":1585,"dateModified":1585},"install-proj-datum-grids-pyqgis","article","Install PROJ Datum Grids","2026-10-02","\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Finstall-proj-datum-grids-pyqgis",{"title":5,"description":1579},"spatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Finstall-proj-datum-grids-pyqgis\u002Findex","RuxxEs_joZvkVaUTxN4-Hpn-dw9pvENRIF6viH1Hw3I",1790966261251]