ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cesium瓦片无缝加载到QGIS:QgsRasterLayer的type=xyz与wms参数解析

Cesium瓦片无缝加载到QGIS:QgsRasterLayer的type=xyz与wms参数解析 这一篇是接着前面几期开发笔记来的主要解决一个很实际的场景Cesium 里能正常显示的二维地图瓦片怎么用 QgsRasterLayer 加载进 QGIS 桌面端。很多人以为 Web 端和桌面端是两套体系瓦片数据互不相通实际上只要搞懂 Cesium 的瓦片 URL 模板和 QGIS 光栅图层的参数格式两边可以做到无缝复用。这篇文章会从原理讲到代码再到排查问题适合正在做 WebGIS 与桌面 GIS 联动、需要在 QGIS 中复现 Cesium 底图的开发者。1. 场景与动机为什么要把 Cesium 的二维瓦片送进 QGIS1.1 实际工作流的痛点我最初遇到这个需求是因为一套基于 Cesium 的展示系统里已经把底图样式调得很好看但后续要做叠加分析、出图打印、矢量编辑这些操作时Web 端还是不够顺手。项目组不想在 QGIS 里重新配一套底图希望直接把 Cesium 里用的那套瓦片数据搬过来保持两边视觉一致同时又能借助 QGIS 的桌面分析能力。这事听起来简单真做起来有几个坎。第一是理解 Cesium 的二维模式到底加载的是什么格式的瓦片第二是知道 QgsRasterLayer 用什么参数去识别这种瓦片源第三是坐标系的问题Cesium 的二维模式默认是 Web 墨卡托投影也就是 EPSG:3857而 QGIS 里项目坐标系不一定是这个加载完还得做投影处理。这些问题不提前想清楚写代码的时候会各种踩坑。1.2 Cesium 二维地图与 QGIS 之间的桥梁Cesium 的二维地图本质上是把三维地球的相机拉到正上方用正交投影的方式渲染平面瓦片。你配置底图时通常会用 UrlTemplateImageryProvider 或类似 provider 去指定瓦片地址比如这样的模板new Cesium.UrlTemplateImageryProvider({ url: https://example.com/map/{z}/{x}/{y}.png, maximumLevel: 19 })这个模板里的{z}、{x}、{y}就是 Web 墨卡托标准瓦片的层级、列号、行号。只要服务器端遵循这套规则客户端是谁并不重要。Cesium 能解析QGIS 同样也能解析。所以两边的桥梁其实就是这条 URL 模板。QGIS 加载这类瓦片源不需要写插件不需要开发自定义数据源直接用 QgsRasterLayer 配合 XYZ 瓦片参数就能搞定。核心是把 URL 模板放进特定的字符串里让 QGS 的光栅数据提供者识别。这个字符串格式就是这篇文章要反复强调的重点。2. 底层原理瓦片坐标与 QgsRasterLayer 的加载机制2.1 瓦片编号规则XYZ 与 TMS 的差异要正确加载瓦片先要搞清楚瓦片坐标是怎么编号的。市面上常见的有两套规则XYZ 规则原点在左上角x 轴向右y 轴向下层级 z 越大瓦片越细。OpenStreetMap、Google 地图、高德地图的 xyz 模式基本都是这套。TMS 规则原点在左下角x 轴向右y 轴向上。Tile Map Service 规范常用这套。这两套规则在 y 轴方向上正好相反。如果你用 XYZ 的坐标去访问 TMS 服务的地址加载出来的瓦片会纵向颠倒看起来就是整个地图上下翻转。反过来也一样。特性XYZTMS原点位置左上角 (0,0)左下角 (0,0)y 轴方向向下递增向上递增常见服务OSM、Google、高德GeoWebCache 默认、部分国产平台QGIS 接入方式typexyz需要额外处理 y 方向翻转QGIS 的typexyz参数遵循的是 XYZ 规则。如果你遇到的是 TMS 服务可以手动做转换把请求地址里的{y}替换成{2^z - 1 - y}但这通常得写代码或者借助中间层代理很少有人在 URL 模板里直接处理。2.2 QgsRasterLayer 与 XYZ 瓦片源typexyz 参数解剖在 QGIS 里QgsRasterLayer 的构造函数是这个样子的QgsRasterLayer(const QString uri, const QString baseName, const QString providerKey)第三个参数 providerKey 是数据提供者的标识。对于在线瓦片我们一般用wms。这容易让人困惑瓦片明明是 xyz 格式为什么要用 wms 这个 providerKey这个后面细说。重点是第一个参数 uri它是一串拼接出来的查询参数里面要包含足够的信息让提供者知道怎么去请求瓦片。最常见的 uri 写法是typexyzurlhttps://example.com/map/{z}/{x}/{y}.pngzmin0zmax19拆开看其实就三部分typexyz声明这是一个 XYZ 瓦片源。url...瓦片地址模板注意{z}、{x}、{y}这些变量是占位符必须原样保留不能提前替换成数字。zmin/zmax可选的层级范围指定这个图层最小的缩放层级和最大的缩放层级。实际使用中可以再加一些参数比如authidEPSG:3857、tmsfalse、featureCount0之类的。但最核心的永远是 type 和 url 这两个。2.3 为什么 providerKey 是 wms刚开始接触这个 API 时我也有点不理解。QGIS 里明明有专门的 XYZ 瓦片连接面板但代码层却要用 wms 这个提供者。原因在于 QGIS 的架构里XYZ 瓦片功能是塞在 wms 数据提供者内部的它相当于是 wms 提供者的一个子模式。所以在 QgsRasterLayer 里想加载 xyz 瓦片providerKey 就填wms然后在 uri 里通过typexyz告诉它走哪个分支。这也解释了为什么很多老代码里能看到类似这样的写法layer QgsRasterLayer(typexyzurlhttps://example.com/map/{z}/{x}/{y}.png, 底图, wms)providerKey 填的就是 wms不要画蛇添足去填xyz填了反而找不到对应的数据提供者。这个设计虽然在 API 上显得有点绕但它也带来了一个好处wms 提供者内部的 HTTP 请求逻辑、缓存管理、重试机制是现成的xyz 瓦片直接复用省了很多底层工作。对于开发者来说只需要记住这个组合即可。3. 代码实现把瓦片 URL 作为光栅图层加载进 QGIS3.1 准备可用瓦片 URL 模板写代码之前先确保你手里有一条可用的瓦片 URL 模板。这里用 OpenStreetMap 找例子说明因为它是公开服务不需要申请密钥任何人都能访问https://tile.openstreetmap.org/{z}/{x}/{y}.png这个模板里z 是缩放级别x 和 y 是瓦片坐标。你可以在浏览器地址栏里试一下把变量替换成具体数字比如https://tile.openstreetmap.org/18/219392/119999.png如果浏览器能正常显示一张 PNG 图片说明模板没问题。这一步很关键因为很多时候加载失败不是代码的锅而是 URL 模板本身就是错的或者服务端有限制。如果你要加载的是 Cesium 里配置的那条地址直接把 Cesium 的 UrlTemplateImageryProvider 里配的 url 拿出来就行。如果里面带{s}这种子域变量需要手动处理要么去掉要么替换成固定的子域编号。QGIS 的 xyz 模板不认{s}这种通配符。3.2 PyQGIS 加载完通资产完整示例代码在 PyQGIS 环境里加载瓦片图层的代码非常简洁。这里直接给出可以根据需要修改的完整示例from qgis.core import QgsRasterLayer, QgsProject, QgsCoordinateReferenceSystem # 瓦片地址模板 tile_url https://tile.openstreetmap.org/{z}/{x}/{y}.png # 拼接 uri 参数 uri typexyzurl tile_url zmin0zmax19 # 创建光栅图层 layer QgsRasterLayer(uri, Cesium 二维底图, wms) if layer.isValid(): # 设置坐标系为 Web 墨卡托 layer.setCrs(QgsCoordinateReferenceSystem.fromEpsgId(3857)) # 把图层加入当前工程 QgsProject.instance().addMapLayer(layer) print(图层加载成功) else: print(图层加载失败请检查 uri 参数)有两点必须提醒。第一tile_url里的{z}、{x}、{y}一定要保留不能提前替换。我见过有人习惯性地调用.format()方法结果把占位符替换成了字符串瓦片自然加载不出来。第二setCrs一定要调用很多在线瓦片服务不会在响应头里显式返回坐标系信息QGIS 默认可能把它当成未知坐标系后续叠加其他数据就会对不上。3.3 C 插件中的加载示例如果你在开发 QGIS C 插件过程其实差不多。核心代码可以这样写#include qgsrasterlayer.h #include qgsproject.h QString tileUrl QStringLiteral(https://tile.openstreetmap.org/{z}/{x}/{y}.png); QString uri QStringLiteral(typexyzurl%1zmin0zmax19).arg(tileUrl); QgsRasterLayer* layer new QgsRasterLayer(uri, QStringLiteral(Cesium底图), QStringLiteral(wms)); if (layer-isValid()) { layer-setCrs(QgsCoordinateReferenceSystem::fromEpsgId(3857)); QgsProject::instance()-addMapLayer(layer); } else { // 处理加载失败的情况 delete layer; }这里要注意内存管理。QgsRasterLayer创建之后如果isValid()返回 false说明图层对象没有绑定有效的数据源这种情况下要记得释放内存否则插件反复运行会导致内存泄漏。C 版本里同样不要对 tileUrl 做任何占位符替换QGIS 会自己在请求瓦片时去替代。你传递给QgsRasterLayer的是一个模板不是具体坐标。3.4 加载后的 CRS 设置与会话管理CRS 这块要多说几句。虽然 Cesium 二维模式默认为 EPSG:3857但你打开的 QGIS 工程投影可能不是它。加载瓦片图层后建议显式设置图层的坐标系避免出现图层虽然显示了但位置不对的情况。在 PyQGIS 里是layer.setCrs(...)在 C 里是layer-setCrs(...)。这个方法只影响图层在工程中的投影定义不会改变瓦片下载逻辑所以随便调用没有副作用。另外如果你要把这个图层保存到 QGIS 工程文件.qgs 或 .qgz直接保存即可。QGIS 会记录下 uri 字符串下次打开工程时自动重新加载。这也是为什么 uri 字符串的规范性很重要——它要能被跨会话持久保存任何一步搞错下次打开工程就是空白图层。4. 常见问题和排查技巧4.1 瓦片间隙问题瓦片间隙是 Web 地图里很经典的问题表现是瓦片与瓦片之间出现细线、白缝或者重复边。在 Cesium 里出现这个问题通常和 WebGL 纹理采样时的浮点精度有关。在 QGIS 里出现间隙的原因稍微不一样更多是渲染引擎对瓦片边缘处理不够平滑。遇到这个问题我建议按顺序排查检查是不是zmin、zmax设置得太宽导致瓦片被过度放大。如果你请求的是 18 级瓦片但显示比例尺已经到 20 级QGIS 会把 18 级瓦片拉伸显示拉伸过程中边缘就容易出问题。尝试在图层属性里将“重采样”方式改为双线性或三次卷积QGIS 对栅格图层的缩放渲染提供了平滑选项能有效减少边缘锯齿。如果你还在用比较老的 QGIS 版本建议升级到 3.28 及以上。新版对 xyz 瓦片的渲染做了不少优化很多旧版本残留的缝隙问题在新版本里直接消失了。4.2 空白图层/加载失败遇到图层加载出来但什么也不显示或者地图画布上只有边框没有内容优先检查这几点。第一uri 字符串有没有拼错。最容易错的地方是url后面的模板本身带着字符。比如有些瓦片服务的地址带 token 参数https://example.com/map/{z}/{x}/{y}.png?tokenabc123这种地址直接拼进 uri 里会导致 QGIS 把tokenabc123也当成 uri 参数的一部分去解析结果整个字符串就乱了。解决办法是对 url 参数做 URL 编码或者用 QUrlQuery 来处理。在 PyQGIS 里一个简单做法是用urllib.parse.quote先编码一遍再拼进去。第二确认 providerKey 是不是 wms。我在代码 review 里经常看到有人按直觉填了xyz然后就加载失败了。虽然 QGIS 有 xyz 的概念但 QgsRasterLayer 的 providerKey 填的必须是wms这是由 QGIS 内部架构决定的。第三检查网络环境。有些在线瓦片服务需要设置代理才能访问或者服务端对 User-Agent 有校验。QGIS 请求瓦片时发送的 User-Agent 可能会被某些服务端拒绝这时候需要到设置里把 QGIS 的访问级别调低或者手动指定 User-Agent。4.3 坐标系错乱叠加不上其他数据这是另一个高频问题表现是瓦片底图能显示但和矢量数据的位置对不上偏差几十米甚至几百米。如果你加载的是国内部分在线地图平台的数据需要注意一个现实存在的问题部分服务对外提供的是经过偏移算法处理过的坐标也就是俗称的“火星坐标”已发布公共概念的偏移坐标系GCJ-02并不是标准 EPSG:3857。这种底图直接加载后叠加 GPS 采集的 WGS84 数据位置会出现稳定偏移这是数据源本身的坐标特性决定的不是 QgsRasterLayer 用错了。遇到这个情况有几个处理思路如果项目数据都是统一的投影坐标系比如都在标准 3857 下那尽量选择提供标准 Web 墨卡托瓦片的数据源。如果一定要用带偏移的底图可以后续对矢量数据做坐标转换或者使用 QGIS 里的第三方坐标纠偏工具。实在不行就把底图当作参考底图使用不做高精度的叠加分析分析操作全部在矢量图层上进行。4.4 性能优化、缓存与离线瓦片瓦片图层卡顿是很多人在 QGIS 里用在线瓦片的直观感受。卡顿的原因主要有两个一是网络请求延迟二是瓦片数据量太大。优化手段可以从这几个方向入手。第一设置合理的 zmin 和 zmax。如果你知道 Cesium 里只显示了 0 到 18 级那 QGIS 里就不要设置到 19 级否则它会在高缩放级别下请求可能根本不存在的瓦片反复报错。第二利用 QGIS 自带的瓦片缓存机制。QgsRasterLayer 的 xyz 瓦片会缓存在本地具体位置取决于操作系统。只要缓存过一次再次加载同一区域的瓦片会直接从本地读取速度快很多。日常开发中可以先在 QGIS 里浏览一遍完整区域把缓存提前建立好后续切图出图就流畅了。第三如果你的场景需要完全离线可以把瓦片预先下载到本地然后用 QGIS 的瓦片文件管理方式加载这超出了 QgsRasterLayer 直接提供的功能范围但思路是一样的瓦片文件是标准的图片文件只要组织好目录结构就能离线使用。5. 几个实操心得代码写起来不难难的是对细节的把控。根据我自己这几轮操作有几件事值得单独拿出来说。第一件事Cesium 里的瓦片地址不一定能直接复制到 QGIS 使用。Cesium 的 UrlTemplateImageryProvider 允许在 URL 模板里写{s}、{style}这类自定义变量QGIS 不认这些。复制过来之前先在浏览器里把变量替换成实际请求地址试一下确认服务端能正常返回图片再谈加载。第二件事用 PyQGIS 调试时最好在加载后打印 layer 的几个关键属性比如dataProvider()、crs()、extent()确认图层确实拿到了数据范围。有时候图层 isValid 返回 true但没有瓦片数据这时候光看 isValid 是不够的。第三件事QGIS 的坐标系设置非常灵活这既是优势也是坑。同一个瓦片图层在 3857 工程下显示正常在 4547 或者其他工程坐标系下也能显示因为 QGIS 会动态重投影。但这个动态重投影会消耗额外性能。如果你只是要快速预览没问题如果要长期使用建议把工程坐标系固定为 3857减少不必要的投影转换开销。这个内容后续还可以扩展的方向比如把 QgsRasterLayer 换成 QgsTileLayer 写自定义瓦片管理插件或者研究如何把本地瓦片包作为数据源加载进 QGIS 实现离线 GIS 方案。这些我后面实战中踩到坑了再继续补笔记。
返回列表