ArcGIS切片缓存接入Cesium:紧凑缓存解包实战指南

发布时间:2026/10/13 10:04:29
ArcGIS切片缓存接入Cesium:紧凑缓存解包实战指南 前阵子接了个三维可视化项目甲方给了一批 ArcGIS 切片缓存数据文件量不小目录结构一眼就是标准切片包。但问题来了这批数据不是 ArcGIS Server 发布的没有 REST 服务地址只有一摞文件夹和几个 xml 文件。Cesium 默认的ArcGisMapServerImageryProvider必须要走服务接口拿这种“裸缓存”一点办法都没有。我一趟折腾下来把紧凑缓存格式、瓦片网格换算、静态服务发布、Cesium 接入全部走过一遍这里把完整思路和可直接复用的代码整理出来给遇到同样问题的人省点时间。不管你是 GIS 开发还是前端转行做三维地球只要手上有一份“没服务、只有文件”的 ArcGIS 切片这篇都适用。1. 先确认数据形态紧凑缓存还是松散缓存1.1 两种缓存目录结构差异ArcGIS 切片缓存打包出来通常有两种形态。打开缓存根目录先别急着写代码花两分钟把目录看明白能避开后面一堆坑。第一种是松散缓存Exploded Cache目录大概是这种结构cache_root/ _alllayers/ L01/ R00000000/ C00000000.jpg C00000001.jpg R00000001/ C00000000.jpg tilemapresource.xml conf.xml这种结构最直观每一个Cxxxxxxx.jpg就对应一张独立瓦片文件名中的列号、路径中的行号、L01中的层级号共同锁定一张瓦片。可以直接用 URL 模板拼路径加载。第二种是紧凑缓存Compact Bundle Cache同样有_alllayers目录但打开最底层文件夹后看到的不是一张张图片而是像这样_alllayers/ L01/ R00000000/ C00000000.jpg R00000001/ C00000000.jpg注意这里C00000000.jpg不是一个普通图片文件而是一个 bundle 包。一个 bundle 文件里打包了 16x16 256 张小瓦片把同一行列范围的图片塞进一个大文件目的是减少磁盘 IO 和文件数量。如果你想用普通图片 URL 去加载浏览器会直接解不出图。还有的项目目录里同时出现_alllayers和_newlayers这通常是不同版本 ArcGIS 产物混在一起。大多数情况下_alllayers是紧凑缓存_newlayers是松散缓存但别完全靠猜建议实际打开看一眼目录末端是 bundle 还是图片。1.2 元数据文件先读 XML 再做任何事缓存根目录下会有tilemapresource.xml这个文件就是整份切片的“身份证”。它包含坐标系、范围、原点、瓦片尺寸、层级范围等关键参数。我每次处理外部切片数据第一件事永远是打开它。TileMap version1.0.0 tilemapservice... SRSEPSG:4326/SRS BoundingBox minx-180 miny-90 maxx180 maxy90/ Origin x-180 y90/ TileFormat width256 height256/ TileSets TileSet dataFrameExtent... href_alllayers minLevel0 maxLevel19/ /TileSets /TileMap关键信息有这么几个SRS坐标系描述常见EPSG:4326WGS84 经纬度和EPSG:3857Web Mercator。这个直接决定后面 Cesium 里用哪种切片方案。Origin x / y切片原点ArcGIS 约定是左上角也就是整个数据范围的西北角。TileFormat width/height每张瓦片像素尺寸绝大多数是 256x256。TileSets minLevel / maxLevel缓存实际存在的层级范围。有些新版本缓存没有tilemapresource.xml但根目录下一定会有conf.xml或conf.cdi。conf.xml里面同样能找到坐标系和切片格式只是字段名不同比如SpatialReference、TileOrigin、CacheTileFormat这些。如果 XML 缺失那就只能根据业务经验和数据范围手动推算切片方案了难度直接上一个台阶。1.3 为什么不能直接用 ArcGisMapServerImageryProvider很多人的第一反应是Cesium 有现成的ArcGisMapServerImageryProvider直接传 URL 不就行了但它的设计前提是有一个正常的 ArcGIS Server 地址提供/rest/services/xxxx/MapServer这种接口。构造 provider 时要先请求服务元数据接口拿到切片方案、层级、范围再走瓦片接口请求图片。而静态文件缓存没有rest/services目录没有 JSON 元数据接口更不支持按瓦片行列号返回单个图片的 REST 协议所以这条路直接堵死。核心思路是绕过 REST 协议把 ArcGIS 的瓦片网格翻译成 Cesium 认可的网格再通过UrlTemplateImageryProvider或自定义 Provider 做图片加载。翻译过程中最重要的就是坐标系和行列号规则对齐。2. 加载方案怎么选解包、静态代理还是自定义 Provider2.1 三种主流路线对比面对一份非 ArcGIS Server 发布的切片缓存实践下来有三条可行路线方案适用数据改动量稳定性备注解包为 ZXY 目录后加载紧凑缓存中等需要一次性转换高会额外占用磁盘空间松散缓存直接 URL 模板加载松散缓存低只需配静态服务高要注意行/列路径补零问题自定义 ImageryProvider 直接读 bundle紧凑缓存且不想复制数据高需要实现底层解析中内存和并发压力大不推荐上生产方案一的本质是把打包的瓦片拆成散文件并按 Cesium 默认路径规则重新组织一劳永逸。方案二最省事如果甲方给的就是松散图片缓存配个 Nginx 就能跑。方案三最“硬核”适合对 bundle 格式很熟悉、且磁盘空间极度紧张的场景但坑非常多我后面单独讲。2.2 我的推荐组合如果数据量在几百 GB 以内推荐方案一。理由很实在Cesium 加载散片图片最稳定走标准 HTTP 请求浏览器和静态服务器都能做缓存CDN 也能直接套。解包之后的目录是标准 ZXY 结构不仅 Cesium 能用Leaflet、OpenLayers、MapLibre 也一样能复用。后续如果换平台、换方案不用再重新折腾原始数据。如果数据量几个 TB磁盘不允许复制一份那就只能考虑方案三或者用 Nginx 做一层动态转换后者可以用服务端程序实时解包但并发一高很容易把 CPU 打满。这个在问题排查部分我还会提。2.3 工具准备Python 3 环境装 Pillow 用来识别图片格式和保存瓦片。Nginx 或者其他静态文件服务器做目录托管和跨域配置。Cesium 任意 1.7x 以上版本推荐保持更新。一份能打开二进制文件的编辑器比如 HxD 或 VS Code 的 Hex 插件排查 bundle 解包异常时会用到。3. 从 tilemapresource.xml 反推出 Cesium 的瓦片网格3.1 核心参数读取拿到tilemapresource.xml后要提取 5 个参数坐标系、范围、原点、瓦片尺寸、层级范围。以下面这份 XML 为例SRSEPSG:3857/SRS BoundingBox minx-20037508.342789 miny-20037508.342789 maxx20037508.342789 maxy20037508.342789/ Origin x-20037508.342789 y20037508.342789/ TileFormat width256 height256/ TileSets minLevel0 maxLevel19/这是标准的 Web Mercator 全球范围切片原点在左上角。ArcGIS 的缓存网格约定是第 0 级一行一列一张瓦片覆盖整个世界范围第 k 级行列数都是2^k。这个规则和 Cesium 的WebMercatorTilingScheme完全一致所以行列号可以直接对应上。如果坐标系是EPSG:4326情况略有不同。第 0 级有两列一行每张瓦片水平覆盖 180 度垂直覆盖 180 度第 k 级时列数是2^(k1)行数是2^k。Cesium 的GeographicTilingScheme默认就是两列一行的起始方案所以也能对应上。3.2 行列号与经纬度的换算关系在写代码之前先把换算关系在纸上推一遍。Web Mercator 下世界范围半宽H 20037508.342789244第 k 级瓦片数n 2^k瓦片分辨率res (2 * H) / (n * 256)左上角原点(x0, y0) (-H, H)瓦片(col, row)的中心点墨卡托坐标x x0 (col 0.5) * res * 256y y0 - (row 0.5) * res * 256墨卡托坐标转经纬度lon x / H * 180lat 2 * atan(exp(y / R)) * 180 / PI - 90其中R 6378137如果看不懂公式没关系下面这段 JS 可以直接用function tileToLonLat(level, col, row, scheme) { if (scheme webMercator) { const H 20037508.342789244; const n Math.pow(2, level); const res (2 * H) / (n * 256); const x -H (col 0.5) * res * 256; const y H - (row 0.5) * res * 256; const R 6378137; const lon x / H * 180; const lat (2 * Math.atan(Math.exp(y / R)) - Math.PI / 2) * 180 / Math.PI; return [lon, lat]; } if (scheme geographic) { const lon -180 (col 0.5) * (180 / Math.pow(2, level)); const lat 90 - (row 0.5) * (180 / Math.pow(2, level)); return [lon, lat]; } }这组公式也是后面排查行列错位问题的核心依据。只要用某个瓦片坐标算出来的中心点和底图的地物对得上就说明网格换算没问题。3.3 在 Cesium 中对应 TilingSchemeCesium 的UrlTemplateImageryProvider不需要手动计算每个瓦片范围但必须告诉它用哪种切片方案。判断方式很简单XML 里SRS是EPSG:3857用WebMercatorTilingSchemeXML 里SRS是EPSG:4326用GeographicTilingScheme对应代码const provider new Cesium.UrlTemplateImageryProvider({ url: http://cache.local/arcgis-cache/{z}/{x}/{y}.png, tilingScheme: new Cesium.WebMercatorTilingScheme(), minimumLevel: 0, maximumLevel: 19 });如果数据是地理坐标系就改成const provider new Cesium.UrlTemplateImageryProvider({ url: http://cache.local/arcgis-cache/{z}/{x}/{y}.png, tilingScheme: new Cesium.GeographicTilingScheme({ numberOfLevelZeroTilesX: 2, numberOfLevelZeroTilesY: 1 }), minimumLevel: 0, maximumLevel: 19 });这里有个隐性条件{z}/{x}/{y}指的分别是层级、列号、行号Cesium 默认按左上角原点的网格取瓦片和 ArcGIS 的网格方向一致。如果加载出来上下颠倒通常是因为数据源是 TMS 规范的左下角原点后面排查部分会详细说。4. 实操路线 A紧凑缓存解包成 ZXY 并接入 Cesium4.1 bundle 文件格式拆解这是整条路线里最核心的一步。一个 bundle 文件开头有 64 字节文件头里面包含版本、存储格式等信息我们不需要解析文件头直接用固定偏移跳过。紧跟着是 256 条索引记录每条记录 5 字节前 4 字节是小端序的瓦片数据偏移量后 1 字节是瓦片数据长度单位是字节。256 条记录对应 16 行 16 列小瓦片索引位置的计算规则是index row * 16 column比如第 3 行第 5 列的瓦片它的索引记录位置就是64 (3 * 16 5) * 5 265从第 265 字节开始读 5 个字节得到数据偏移和长度再从那个偏移位置读出 JPEG 或 PNG 的原始字节流。这里要提一个容易踩的坑长度字段只有 1 字节最大值是 255但压缩后的瓦片数据经常超过 255 字节。实际项目中我看到的数据索引记录的长度字段在旧版 bundle 里表示真实长度但也有的版本做了编码处理如果解出来的图片大小不对先检查一下 conf.xml 里的缓存版本标识。我处理过的大多数数据5 字节索引规则都适用不同版本间的细微差异主要体现在 header 和偏移字段上遇到异常不要慌用十六进制编辑器对比正常瓦片和异常瓦片的数据布局就能定位。4.2 Python 解包脚本下面是一个可以直接改路径运行的解包脚本示例import struct import os from PIL import Image import io def extract_tile_from_bundle(data, row, col): if len(data) 64: return None index row * 16 col rec_offset 64 index * 5 if rec_offset 5 len(data): return None offset, length struct.unpack_from(IB, data, rec_offset) if offset 0 or length 0: return None if offset length len(data): return None return data[offset:offset length] def convert_bundle(bundle_path, out_row_base, out_col_base, out_dir): with open(bundle_path, rb) as f: data f.read() for r in range(16): for c in range(16): raw extract_tile_from_bundle(data, r, c) if raw is None: continue # 通过文件头判断图片类型 ext jpg if raw[:8] b\x89PNG\r\n\x1a\n: ext png img Image.open(io.BytesIO(raw)) global_row out_row_base r global_col out_col_base c out_path os.path.join(out_dir, str(global_col), f{global_row}.{ext}) os.makedirs(os.path.dirname(out_path), exist_okTrue) img.save(out_path) def convert_level(src_level_dir, dst_level_dir, level): for root, dirs, files in os.walk(src_level_dir): for fn in files: if not fn.lower().startswith(c): continue row_bundle int(os.path.basename(root)[1:]) # R00000000 - 0 col_bundle int(fn[1:9]) # C00000000 - 0 bundle_path os.path.join(root, fn) convert_bundle(bundle_path, row_bundle * 16, col_bundle * 16, dst_level_dir)使用方式python convert_bundle.py \ --src /data/cache/_alllayers/L05 \ --dst /data/zxy/05 \ --level 5几个注意事项先跑一个小范围验证比如只处理L05/R00000001/C00000002.jpg一个 bundle然后用 Cesium 加载看一眼有没有错位再全量跑。写入时目录按{z}/{col}/{row}.png组织也就是列号做目录、行号做文件名这和 Cesium 默认{z}/{x}/{y}正好匹配。如果你坚持用L/R/C原目录要改 URL 模板还绕不开补零问题。脚本没有做断点续传全量转换失败就从头跑生产环境建议加一个os.path.exists(out_path) and os.path.getsize(out_path) 0的判断做成可续跑脚本。4.3 Nginx 静态服务配置解包完成后用 Nginx 把根目录暴露出去。最简单的配置server { listen 8080; server_name cache.local; root /data/zxy; add_header Access-Control-Allow-Origin *; add_header Cache-Control public, max-age86400; location ~ \.(png|jpg)$ { expires 7d; access_log off; } }这里Access-Control-Allow-Origin *很重要。如果 Cesium 页面域名是app.local瓦片服务域名是cache.local跨域请求会被浏览器拦截加了响应头才能正常加载。生产环境建议把*改成具体页面域名别偷懒。4.4 Cesium 端加载与测试接入代码其实很短const viewer new Cesium.Viewer(cesiumContainer, { baseLayer: false, timeline: false, animation: false }); const provider new Cesium.UrlTemplateImageryProvider({ url: http://cache.local/arcgis-cache/{z}/{x}/{y}.png, tilingScheme: new Cesium.WebMercatorTilingScheme(), minimumLevel: 0, maximumLevel: 19 }); viewer.imageryLayers.addImageryProvider(provider);测试时先把相机飞到瓦片方案定义的范围内确认能加载出图再验证几个关键瓦片的行列号对应地物是否正确。一个简单方法把范围缩到某栋建筑然后读取当前相机的经纬度套用前面的公式反算出行列号直接从 Nginx 拉对应瓦片看是不是同一位置。4.5 松散缓存直接用 URL 模板加载如果是松散缓存理论上可以直接把url配成url: http://cache.local/original-cache/_alllayers/{z}/R{y}/C{x}.jpg但问题马上来了ArcGIS 目录名都是L01、R00000000、C00000000这种补零格式Cesium 的{z}输出的是1不是01{y}也不是 8 位补零格式。一旦目录层级超过 9或者行列号超过 9路径就对不上。绕开这个问题有两个办法一个是生成一份按日期或行号重命名的目录副本本质还是解包另一个是在 Nginx 层做前缀匹配把请求映射到真实目录。后者在简单场景能跑但补零逻辑非常繁琐我不建议。除非数据量实在太大、不允许复制否则直接走方案 A 的解包脚本最省心。5. 实操路线 B免解包的 Cesium 自定义 Provider 思路5.1 什么场景值得走这条路如果缓存数据有几个 TB磁盘上真的放不下第二份或者项目周期紧到没时间全量转换才考虑自定义 Provider 直接读 bundle。这个方案能在 Cesium 请求瓦片时实时解析 bundle 并返回对应的小图片省掉转换过程但代价是开发量、调试成本和运行期并发压力都显著上升。我在项目中尝试过这个方案最终没有上生产原因是 Cesium 的ImageryProvider接口需要实现的方法比较多requestImage返回的还要是合法图片资源调试成本高。如果你只是临时查看数据完全可以用其他工具实时解包不必动 Cesium 底层。5.2 核心代码骨架自定义 Provider 的骨架大致如下class ArcGisBundleProvider { constructor(options) { this._baseUrl options.url; this.tilingScheme options.tilingScheme; this.minimumLevel options.minimumLevel || 0; this.maximumLevel options.maximumLevel || 19; this.tileWidth 256; this.tileHeight 256; this.ready true; } requestImage(x, y, level) { const bundlePath this._getBundlePath(x, y, level); const indexInBundle this._getIndexInBundle(x, y); return fetch(bundlePath) .then(res res.arrayBuffer()) .then(buffer this._extractTile(buffer, indexInBundle)) .then(blob URL.createObjectURL(blob)); } _getBundlePath(x, y, level) { const rowBundle Math.floor(y / 16); const colBundle Math.floor(x / 16); const l String(level).padStart(2, 0); const r String(rowBundle).padStart(8, 0); const c String(colBundle).padStart(8, 0); return ${this._baseUrl}/L${l}/R${r}/C${c}.jpg; } _getIndexInBundle(x, y) { return (y % 16) * 16 (x % 16); } _extractTile(buffer, index) { const data new Uint8Array(buffer); const recOffset 64 index * 5; const offset data[recOffset] | (data[recOffset 1] 8) | (data[recOffset 2] 16) | (data[recOffset 3] 24); const length data[recOffset 4]; if (offset 0 || length 0) { return fetch(data:image/gif;base64,R0lGODlhAQABAAAAACw).then(r r.blob()); } const slice data.slice(offset, offset length); return new Blob([slice], { type: image/jpeg }); } }这段代码表达的是核心思路直接跑到生产前还要考虑几件事requestImage中fetch的并发控制bundle 资源缓存到内存或 IndexedDB以及缺失瓦片的占位处理。只有小范围单机展示时它才是合理的。5.3 我在这条路上踩过的坑缺失瓦片直接用undefined返回Cesium 可能不渲染该区域但如果你用的是 Canvas 拼接方式会留黑边。我最后是返回一个 1x1 透明 GIF 数据 URL占位干净。每个瓦片请求都要先拉整个 bundle16x16 瓦片共用一个 bundle如果不做内存缓存加载一屏瓦片会产生大量重复请求性能直接崩溃。bundle 不是 JPEG 开头不能直接塞给浏览器。必须先解出小瓦片再包装成Blob很多刚接触这块的人在这一步会莫名拿到解析失败报错。路由 B 并不是完全不能落地但要把它当成一个小型中间件来做而不是一段临时脚本。6. 实战问题排查与避坑速查6.1 行列上下颠倒或者左右镜像最典型的表现是瓦片能加载但地物位置完全不对或者南北翻转。先说结论绝大多数情况是切片方案选错了。如果数据源是 TMS 规范原点在左下角Cesium 默认按左上角原点取瓦片行号方向就反了需要把行号做一次翻转const flippedY Math.pow(2, z) - 1 - y;但 ArcGIS 的切片方案原点就是左上角理论上不需要翻转。所以遇到这个问题先回头打开tilemapresource.xml确认Origin是不是左上角以及 SRS 确实是 3857 或 4326。别改了半天代码结果数据本身是 4490 或者其他自定义坐标系。6.2 层级目录对不上有时瓦片显示空白打开 Network 面板发现请求的路径是L1、L2但磁盘上存的是L01、L02。这就是补零问题。解包方案不会遇到但如果你坚持走松散缓存并配了自定义 URL 模板就得自己处理层级补零。我的建议是尽量在数据准备阶段就把目录规范成0、1、2、10、11这种纯数字层级后续接入框架时零心智负担。6.3 bundle 解出的图片花屏或损坏出现花屏通常不是压缩算法问题而是索引读取位置算错了。最常见的坑是混淆了 bundle 内瓦片索引的排列顺序把row * 16 col写成了col * 16 row。逐字节检查一下索引记录里偏移量和真实 JPEG 文件头的位置是否一致就能定位。另外部分 bundle 的索引记录不是固定 5 字节或者 header 不是 64 字节这在旧版本和某些第三方导出工具生成的缓存里出现过。排查办法是用十六进制编辑器打开 bundle找到FF D8开头的 JPEG 位置再反推索引偏移的规律。6.4 跨域导致图片加载失败Cesium 加载跨域图片时如果服务端没返回 CORS 头浏览器会直接拦截。现象是瓦片区域灰白控制台报错指向图片加载失败。处理方式有两个静态文件服务器加上Access-Control-Allow-Origin响应头。如果瓦片服务域名和页面域名相同就不存在这个问题但生产环境经常因为动静分离导致跨域所以 Nginx 配置里加响应头是基操。6.5 快速排查表现象优先排查方向处理方法整屏无图网格方案选错、路径拼错先拉一张瓦片 URL 看能否访问位置偏移坐标系和切片方案不匹配核对 tilemapresource.xml 的 SRS上下翻转原点方向错误确认 Origin 是左上角某层级不出图层级范围或目录名补零检查 minimumLevel 和 maximumLevelbundle 解出花屏索引偏移算法错误用十六进制编辑器对照瓦片头请求失败跨域CORS 未配置服务端加响应头上面这些坑我基本都踩过一轮每次都在最不起眼的地方翻车。像补零这种问题代码层面基本看不出毛病但一请求就 404只有开着控制台盯 Network 才能发现。这个内容其实还可以继续扩展比如把解包脚本改成多进程、加断点续传或者直接在 Nginx 层用sub_filter做路径映射。但如果你手头的需求只是“把非 ArcGIS Server 发布的切片接进 Cesium”把前面的路线 A 完整跑通就已经能解决 90% 的问题了。根据我的个人经验拿到任何缓存数据先看 XML 再动手先在单个层级上验证方案再全量处理这套节奏能帮你避开大量返工。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询