Cesium加载MVT数据

标签: Cesium 分类: Gis 创建时间:2023-05-31 02:29:24 更新时间:2023-10-20 11:23:25

1.前言

MVT数据格式,就是Mapbox开发的一种矢量数据格式,可以自定义矢量数据样式,自定义丰富的地图风格。Cesium的标准是3dtile,主打的就是三维数据格式。关于Cesium加载Mvt格式的方法,其实网上并不是很多,而且Cesium对于支持MVT格式的热情也不是很高,主要的精力还是研究 3dtiles 格式。

  • 1.cesium+mapbox,这个方法解析和加载的速度都很快,不过只支持3857和900913,不支持2000坐标系。
  • 2.cesium+openlayer,利用openlayer的方法将mvt绘制到canvas上,然后传给cesium进行渲染,这个方法不限制坐标系,支持4490,4326,3857,900913等。
  • 3.使用超图封装的cesium加载。

根据我的理解,其实使用cesium+openlayer的方式更加的舒服,虽然不如cesium+mapbox的性能更好,但是openlayer可以模块化开发,需要什么模块,只需要引入 render 就可以了,但是 mapbox 要想单独提取render,还需要使用自定义构建,比如 Mapbox-vector-tiles-basic-js-renderer 这个仓库。或者是在 两个 MVTImageryProvider 参考中,都需要一个 mapboxRenderer ,这个就是独立定制的了。

所以最终我决定改造 ol 版本的,因为 mars3d 提供的 ol 版本不是最新的,我想着先尝试修改一下。

参考文章:
1.Cesium 加载mapboxgl最新失量切片(mvt) 这篇借助于Openlayer进行了加载,和 mars3d 的加载方式差不多,里面也做了变换。
2.cesium加载geoserver发布的mvt服务
3.mars3d-link-supermap Mars3d中使用Supmap Cesium进行兼容的仓库。
5.mapbox 扩展

2.MVT格式

MVT图层包括以下几个主要的部分。

参考文章:
1.MVT: Mapbox Vector Tiles
2.矢量瓦片 extent说明:瓦片的extent只是指明了瓦片的范围,并不是瓦片的渲染之后的大小。例如一张extent为4096的矢量瓦片,并不意味着最终渲染出来的是一张4096 X 4096大小的图片。渲染出的图片大小并不由extent决定,通常会被渲染为256 X 256或512 X 512大小的图片。假设将一张extent为4096的矢量瓦片渲染为256 X 256大小的图片,那么(0, 0)~(16, 16)坐标范围内的点都将被综合到图片上的(0, 0)像素点。
3.矢量瓦片标准 图层必须包含一个extent字段,表示瓦片的宽度和高度,以整数表示。矢量瓦片中的几何坐标可以超出extent定义的范围。超出extent范围的几何要素被经常用来作为缓冲区,以渲染重叠在多块相邻瓦片上的要素。
4.浅谈Mapbox开源技术
5.02. 矢量瓦片并行构建与分布式存储模型研究
6.python爬取pbf格式的矢量瓦片并转换为shp使用 这篇文章用python下载了pbf的格式文件,并进行了拼接。

3.瓦片原理

瓦片原理,就是通过一张张图片进行贴图,但是不同的坐标系有不同的计算方法。

参考文章:
1.WMTS服务及地图瓦片原理
2.瓦片(Tile)行列号计算方法
3.GIS开发:切片格式说明(翻译)-wgs84 wgs84的坐标系范围是:[-180,-90,180,90],单位是度。gdal2tiles地图切片
4. WMTS地图服务每一层级分辨率 这里没有给出计算公式,直接列举了 3857 和 4326 坐标系的瓦片分辨率

1.Cesium

经过不断的学习和探索,还是没有明白地图瓦片的原理,但是在 UrlTemplateImageryProvider.js 中,有这么一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const longitudeLatitudeProjectedScratch = new Cartesian3();

function computeIJ(imageryProvider, x, y, level, longitude, latitude, format) {
if (ijScratchComputed) {
return;
}

computeLongitudeLatitudeProjected(
imageryProvider,
x,
y,
level,
longitude,
latitude
);
const projected = longitudeLatitudeProjectedScratch;

const rectangle = imageryProvider.tilingScheme.tileXYToNativeRectangle(
x,
y,
level,
rectangleScratch
);
ijScratch.x =
((imageryProvider.tileWidth * (projected.x - rectangle.west)) /
rectangle.width) |
0;
ijScratch.y =
((imageryProvider.tileHeight * (rectangle.north - projected.y)) /
rectangle.height) |
0;
ijScratchComputed = true;
}
参考文章:
1.Cesium原理篇:5最长的一帧之影像
2. Cesium笔记(3):基本控件简介—ImageryProvider地图瓦片地图配 这里讲了多种 provider 的用法,每一个Provider都对应一个tilingScheme,支持经纬度和墨卡托两种投影方式,默认是全球范围,用户也可以指定其范围。
3.cesium加载坐标系为4490的arcgis瓦片图 这里有几个地方,一个就是 _tilingScheme,一个就是 GeographicTilingScheme.prototype.getNumberOfXTilesAtLevel
4.cesium加载arcgis离线松散瓦片(EPSG:4326, EPSG:4490) 这里使用Cesium.UrlTemplateImageryProvider自定义瓦片规则进行加载。重写了 customTags 和 tilingScheme 。
5.ImageryLayerCollection类 Cesium.ImageryProvider类及其子类封装了加载各种影像图层的方法,其中Cesium.ImageryProvider类是抽象类、基类或者可将其理解为接口,它不能被直接实例化。上图中Cesium.ImageryLayer的第一个参数就是需要的图层数据,我们可以把ImageryProvider看作是影像图层的数据源(包裹在ImageryLayer类内部),我们想使用哪种影像图层数据或服务就用对应的ImageryProvider子类去加载
6.Cesium 4490 解决方案 2000.xml 和2000天地图切片方案(5-16级).xml 区别在于2000有21级切片,2000天地图只有20级切片,2000的切片方案第0级是多出来的一级,其他级别与2000天地图一致。1.修改Cesium 源码WebMapTileServiceImageryProvider,判断WMTS服务的切片级别,请求数据时调整级别,这个方案比较麻烦,对源码的理解要比较深入,才能修改,暂时没用这种方式。2.修改数据。3.互联网2000的切片方案,原点是400,-400,似乎也加载不了,有数据的时候可以测试下。

OpenLayer

使用 openlayer 进行 mvt 的渲染,在官网上也有很多的例子。

参考文章:
1.openlayers6:入门基础(二)之加载图层 地图容器(Map)与图层(Layer)的渲染有Canvas、WebGL两种类型,分别由ol.renderer.Map与ol.renderer.Layer实现。
2.openlayers3瓦片加载的源码浅析与小结 这里有瓦片渲染的原理解析。getTileRangeForExtentAndResolution获取可视范围内的瓦片范围;calculateTileRanges->循环遍历resolutions,调用 getTileRangeForExtentAndZ,根据extent计算瓦片范围;getTileRangeForExtentAndResolution计算瓦片的范围。这里还有TMS瓦片、WMTS瓦片加载、百度地图瓦片、腾讯地图瓦片
3.OpenLayers教程十:多源数据加载之瓦片地图原理二
4.OpenLayers6学习笔记(三)—— 瓦片地图基础知识 这篇文章我觉得写的挺好的,还有TileDebug,比例尺=0.0254÷(分辨率×96)。在WebGIS中使用的在线瓦片地图采用Web墨卡托(Mercator)投影坐标系(OpenLayers默认使用EPSG:3857),经过投影后整个地球是一个正方形,范围为经度[-180°, 180°],纬度[-85°, 85°],单位为度。对应的Web墨卡托投影坐标系的范围为x[-20037508.3427892, 20037508.3427892]、y[-20037508.3427892, 20037508.3427892],x、y方向上的各层级瓦片地图分辨率计算公式可以归纳为:resolution = rang÷(256×2^z).

4.插件封装

插件的封装,就是用符合 Cesium 的标准进行的二次开发,比如如果要自定义 ImageryProvider, 可以查看官方的代码 但是这个官方的代码,没有使用es6模块进行类的定义,还在使用使用 ES5 的方式进行类的扩展。

5.样式解析

可以使用 openlayers / ol-mapbox-style 这个库进行mapbox的样式解析,但是这个只适用于 openlayer 。

(1) 而且无法解析 mapbox style 中的 glyphs 属性。“ol-mapbox-style cannot use PBF/SDF glyphs for text-font layout property, as defined in the Mapbox Style specification. Instead, it relies on web fonts. A ol:webfonts metadata property can be set on the root of the Style object to specify a location for webfonts, e.g.”

(2) stylefunction 函数中的 spriteData 参数无效,必须要通过 spriteimageUrl 指定精灵图的地址,才能使 精灵图生效。styleFunction 会下载图片,并调用 oenlayer 图层的 change 方法修改图层。

参考文章:
1.glyphs 这是对 mapbox 字体的解释。
2.Mapbox GL JS学习笔记六:Mapbox样式字体本地化方法 使用python语言将字体进行了本地化部署

6.加载示例

下面是我研究的几个源码,最后我选择了着重研究 cesium + openlayer

cesium-mvtimageryprovider

kikitte / MVTImageryProvider

mvt-imagery-provider

hongfaqiu / MVTImageryProvider 这是另外的一个MVTImageProvider的改造

mars3d-PbfLayer

mars3d-mapbox 这个地方是 mars3d 改造的 mapbox-gl 的例子,但是没有运行起来。我查了下资料,找到了landtechnologies / Mapbox-vector-tiles-basic-js-renderer,也就是所有基于mapbox在cesium中渲染mvt的,都需要引入这个库。这里主要就是 重载了 src/basic/renderer.js 这个代码,实现了独立渲染。

参考文章:
1.Cesium加载mapbox矢量瓦片源码实测可用 这里也用了一个 Mapbox.BasicRender 的东西,其实是一个库。

mars3d-PbfolLayer

pbf-ol 这里借助于 openlayer 实现的 pbf 加载。但是我查看了源码之后,发现有些方法在最新的openlayer中并不存在,比如 ol.render.canvas.ReplayGroup 这个方法,只在 5.1.3 版本中存在。

(1) openlayer的styleFunction,渲染的是一个openlayer图层,而不是一个feature,所以不能使用 ol-mapbox-style 这个库。

后来我还是极力的把这个库给用上了。

(2) 重写 ImageProvider 的 requestImage 方法,获取每一个切片。 ImageProvider是一个虚基类,所有的 Provider 都是重写了这个类。ImageryLayer.js 源码中调用了 ImageProvider的requestImage方法。ImageryLayer.prototype._requestImagery 实现了 doRequest()、success()、和 failure() 方法,分别实现了请求切片,和请求切片失败时的处理。

函数中需要一个 imager 参数,路径在 engine/Source/Scene/Imagery.js,可以通过这个类,继续的深入的探讨关于如何绘制一个瓦片的过程。

(3) 然后使用 openlayer 提供的方法,将其渲染为一个 canvas 并返回,这里同样有两种方法,比较简单的一种就是使用 ol.render.toContext方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var canvas = document.createElement("canvas")
canvas.width = that._tileWidth
canvas.height = that._tileHeight
var vectorContext = canvas.getContext("2d")
var features = that._mvtParser.readFeatures(arrayBuffer)
var styleFun = that._styleClass.getStyle()
var extent = [0, 0, 4096, 4096]
for (var i = 0; i < features.length; i++) {
var feature = features[i]
var styles = styleFun(features[i], that._resolutions[level])
for (var j = 0; j < styles.length; j++) {
const render = toContext(vectorContext);
render.drawFeature(feature,styles[j]);
}
}

if (that._tileQueue.count > that._cacheSize) {
trimTiles(that._tileQueue, that._cacheSize / 2)
}
canvas.xMvt = x
canvas.yMvt = y
canvas.zMvt = level+2
that._tileQueue.markTileRendered(canvas)
return canvas

这里同样有一个问题,就是读取的数据如何绘制到一个没有坐标系的 cavas 上。 后来我经过尝试,发现这个 canvas

参考文章:
1.toContext Binds a Canvas Immediate API to a canvas context, to allow drawing geometries to the context’s canvas. 我发现了这个函数更加的简单,就是把一个feature,渲染到一个 canvas 上面
2.OpenLayers官方示例详解十一之在自定义canvas元素上渲染OpenLayers的几何图形(Render geometries to a canvas)

(4) 这里需要有一个地方注意,就是在使用 format.MVT 进行转换的时候,还有一个坐标系的问题。

1
2
3
4
5
6
7
8
9
10
11
12
var features = that._mvtParser.readFeatures(arrayBuffer,{
dataProjection: new Projection({
code: 'TILE_PIXELS',
units: 'tile-pixels',
extent: [0, 0, 4096, 4096]
}),
featureProjection:new Projection({
code: 'TILE_PIXELS',
units: 'tile-pixels',
extent: [0, 0, 256, 256]
})
})

我经过大量的实验,发现在创建 canvas 的时候,使用宽高 4096 可以基本解决我的问题。

参考文章:
1.geojson-vt integration 这里用的是geojson进行解析的,还有dataProjection的设置。
2.ol/proj/Projection~Projection

(5) 还有一种方法,就是使用 openlayer 源码中提供的方法,这个具体的原理还是有些厉害的,VectorTileLayer.js,但是比较复杂,我看到的多数的目前的代码,都是以这种形式展开的。我也尝试过,目前是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import MVT from 'ol/format/MVT.js';
import CanvasBuilderGroup from 'ol/render/canvas/BuilderGroup.js';
import CanvasExecutorGroup from 'ol/render/canvas/ExecutorGroup.js';
import {renderFeature} from 'ol/renderer/vector.js';

MvtImageryProvider.prototype.requestImage = function (x, y, level, request) {
// .... 省略很多代码

const builderGroup = new CanvasBuilderGroup(0,extent,resolution,this._pixelRatio);

for (var i = 0; i < features.length; i++) {
var feature = features[i]
var styles = styleFun(features[i], that._resolutions[level])
for (var j = 0; j < styles.length; j++) {
// ol.renderer.vector.renderFeature_(_replayGroup, feature, styles[j], 16)
renderFeature(builderGroup,feature,styles[j],16)
}
}
// _replayGroup.finish()
const executorGroupInstructions=builderGroup.finish();
const renderingReplayGroup = new CanvasExecutorGroup(extent,resolution,this._pixelRatio,null,executorGroupInstructions);

//this._pixelRatio = 1
// this._transform = [0.125, 0, 0, 0.125, 0, 0],这是512x512点瓦片,
renderingReplayGroup.execute(vectorContext,that._pixelRatio, that._transform,0,true);

// .... 省略很多代码
}
参考文章:
1.ol5 ReplayGroup 这里遇到了一样的问题。
2.Openlayers 3 - How to garbage collect Canvas ReplayGroup objects?
3.Static Image is not cleared when change projection 这里有 resolutionsFromExtent 的源码,可以借鉴。
4.Openlayers源码阅读(八):要素Feature渲染过程 这里讲了openlayer渲染feature的过程。

问题

(1) missing required property “version”
除了上面的问题,还有就是 ”missing required property ‘layers’“、”missing required property ‘sources’“,都是一起冒出来的

(2) requestImage 请求切片z不正确
在我使用代码调用在重写 requestImage 方法的时候,我请求的是4490的坐标系,这个level必须要在原有基础上进行 +2 处理,这个操作让我始终无法理解,但是我最后还是实验出来了这个方法,算是瞎猫碰上了死耗子。因为我对比了使用mapbox加载矢量切片的路径,还有使用Cesium进行矢量切片的路径,发现7级、8级的时候,切片的x和y总是非常的大,这让我非常的郁闷。后来我干脆就直接输出了在地图上绘制了瓦片信息,进行问题查找。我在这个函数中进行了方里网格的绘制,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
MVTImageryProvider.prototype.requestImage = function (x, y, level, request) {

var url = this._url
// 这里不知道为什么,如果直接写level的话,那么我的4326的坐标系,就无法获取正确的图层
level=level+2
var reverseY = this._tilingScheme.getNumberOfYTilesAtLevel(level)-y-1;
// 拼接url
url = url.replace("{x}", x).replace("{y}", y).replace("{reverseY}", reverseY).replace("{z}", level).replace("{k}", this._key)
var resource = Cesium.Resource.createIfNeeded(url)

// ... 省略部分代码

var canvas = document.createElement("canvas")
// 这里的width为什么是 4096,暂时没有理论支撑,不太清楚。
canvas.width = 4096
canvas.height = 4096
var ctx = canvas.getContext("2d")

// 绘制方里网格
ctx.moveTo( 0, 0 ); //起点
ctx.lineTo( 0, that._tileWidth);
ctx.lineTo( that._tileWidth, that._tileHeight );
ctx.lineTo( that._tileHeight, 0 );
// 颜色设置,必须放在绘制之前
ctx.strokeStyle = 'red';
// 线宽设置,必须放在绘制之前
ctx.lineWidth = 1;
ctx.stroke(); //描边(绘制)
// 绘制文本
ctx.font = "30px serif";
//2. 使用`fillStyle`设置字体颜色。
ctx.fillStyle = "#00AAAA";
//3. 使用`fillText()`方法显示字体。
let text="z:"+level+",x:"+x+",y:"+y
ctx.fillText(text,0,50);

return canvas;
}
参考文章:
1.Cesium 之加载ArcGIS Server 4490切片服务(含orgin -400 400) ArcGisMapServerImageryProvider直接加载CGCS2000的4490坐标系,虽然可以使用WebMapTileServiceImageryProvider加载符合OGC标准的WMTS类型,但只能针对于原点在orgin X: -180.0Y: 90.0的情况,对于原点在orgin X: -400.0Y: 400.0时还是无法正常加载,为了能够实现加载,以下通过修改源码方式来达到目的。
2.Cesium加载矢量数据探索——从geojson到矢量切片 目中提供的mapbox-gl并不是真正的mapbox项目中的mapbox-gl.js文件,而是由另一位大神,根据mapbox源码中的渲染方式进行改进,从而可以实现基于mapbox的方式加载矢量切片的图层,该项目最终打包命名为mapbox-gl.js

(3) loseContext: context lost
通过自定义 imageryprovider,然后添加到 layers中,直接将内存撑爆了:Out of Memory。下面的代码看起来没有毛病。

1
2
3
4
5
6
7
8
9
10
11
12
const provider = new MvtImageryProvider({
url: "https://map.hongjing.fpi-inc.site:8081/maps/tdt_jj/{z}/{x}/{y}.mvt",
styleConfig: sourceConfig["osm_jj"].styleConfig,
minimumLevel: 4,
maximumLevel: 20
})
let cesium = mars3d.Cesium
console.log(cesium)
const viewer = this.map.viewer
//通过imageryLayers获取图层列表集合
const imageryLayers = viewer.scene.imageryLayers;
imageryLayers.addImageryProvider(provider);

【解决方案】
这个问题也是让我非常的郁闷,等我关了电脑,第二天放弃使用 ToDesk 远程桌面进行远程开发之后,这个问题就没有出现了。另外非常的郁闷,上一次也是出现了这个问题,vscode 总是崩溃,而且远程调试的时候,无论是chrome还是firefox,都会出现问题。

这个问题我记得我上一个星期在进行远程开发的时候也遇到过了,《{ post_link Tensorflow实战之常见问题 Tensorflow实战之常见问题 }》这篇文章中也出现的类似的现象,就是vscode总是崩溃,总是显示OOM,关闭之后,重启也没有用,解决方法也是类似,我头天晚上关机,关了远程,然后第二天到电脑实地进行训练,结果不再出现这个问了,真的非常的令人头疼。

参考文章:
1.Cesium学习之添加基本图层(adding Imagery Layers))
2.cesium 解决 WebGLRenderingContext 丢失问题 一般情况下,一个 viewer 对应着一个 webgl context,如果有多个就会存在多个 WebGLRenderingContext。
3.Cesium losing webgl context in Chrome
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
bibichuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。