Cesium加载MVT数据二

标签: Cesium 分类: Gis 创建时间:2023-06-09 16:18:19 更新时间:2023-10-20 11:23:25

1.前言

上一节根据笼统的处理,基本上对使用 openlayer 使Cesium可以加载MVT数据有了一个大致的思路,这篇文章主要用来深化一下,并提供源代码。需要解决的问题主要有下面几个方面:
(1) 获取 mvt 瓦片数据
通过自定义 ImageryProvider,可以实现加载 mvt 格式的瓦片数据。根据瓦片构造原理,拼接合适的 url,通过调用 Cesium.Resource.createIfNeeded,通过一个url获取瓦片资源,然后使用 resource.fetchArrayBuffer() 进行数据处理。

(2) 解析 mvt 数据
解析mvt数据,主要是通过 ol/format/MVT 这个解析库实现的。根据获取到 arrayBuffer 数据,可以通过 ol/format/MVT 的 readFeatures 从一个 arrayBuffer 中获取到所有的 feature 信息。

(3) 解析 mapbox 的样式信息
如何解析 mapbox 的样式信息,将其应用到主要是通过 ol-mapbox-style 这个库来实现。但是这个库也有些问题:

  • 字体的渲染
    在官方的库文件中,并不能支持 mapbox 的 style 的 glyphs 配置为 pbf 格式。

  • 精灵图的绘制
    在库中,精灵图的加载通过 image 进行控制,异步获取图片数据之后,通过调用图层的 change 方法,触发图层数据的更新,但是这步操作,在 Cesium 里面是没有的。

  • 各个图层样式分离
    mapbox的样式文件中 layers 是一个数组,多个图层可以共用一个 source。这点在Cesium中不好实现,因为一个Cesium图层,对应的应该是一个source数据源,一个数据源的mvt切片包中,包含了多个feature,每一个feature分属不同的层。也就是说在创建Cesium时,一个图层对应一个 provider。

(4) 在 Cesium 上绘制整个图层
在Cesium绘制图层也同样通过 requestImage 函数,这个函数在获取数据后,可以通过返回一个 canvas 进行图层的绘制。也就是说可以讲获取到的 mvt 解析数据后,绘制到一个 canvas 上,这样就可以在 requestImage 进行返回,供 Cesium 进行渲染了。

总结:
Cesium渲染mvt数据,用的工具主要是 openlayer的 mvt 解析库 和 ol-mapbox-style 样式解析库, 需要自定义的就是 Cesium 中的 ImageryProvider 这个类,通过这个类来渲染自己的图层。

参考文章:
1.Cesium笔记(3):基本控件简介—ImageryProvider地图瓦片地图配 支持的瓦片格式 wms、TMS、WMTS、ArcGIS、Bing Maps、Google Earth、Mapbox、OpenStreetMap
2.Cesium 高性能扩展之自定义地图主题 这里主要讲了 加深对 Cesium 影像加载(ImageryLayer和ImageryProvider)的理解;加深对DrawCommand的理解;了解Cesium实现RTT的基本流程。
5.从零打造一个Web地图引擎 这里讲了在创建一个地图引擎的时候,常用的功能,比如地图分辨率获取,地图瓦片的加载,还有地图的拖动

2.MvtImageryProvider实现

这里我贴出来自己的代码,因为我使用的是 4490 的坐标系,所以这里的 tillingScheme 以及分辨率都是在 4490 的基础上实现的。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import MVT from 'ol/format/MVT.js';
import {toContext} from 'ol/render';

/**
* 创建 mvt provider
* @param {*} options
*/
function MvtImageryProvider(options) {
options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT)
this.options = options
// 瓦片的大小
this._tileWidth = Cesium.defaultValue(options.tileWidth, 256)
this._tileHeight = Cesium.defaultValue(options.tileHeight, 256)
this._minimumLevel = Cesium.defaultValue(options.minimumLevel, 1) // 最小显示级别
this._maximumLevel = Cesium.defaultValue(options.maximumLevel, 20) // 最大显示级别
this._rectangle = Cesium.Rectangle.fromDegrees(-180, -90, 180, 90)
// 定义椭球体
let cgs2000Ellipsolid = new Cesium.Ellipsoid(6378137.0, 6378137.0, 6356752.31414035585)
let myGeographicTilingScheme = new Cesium.GeographicTilingScheme({
ellipsoid: cgs2000Ellipsolid,
rectangle: this._rectangle,
numberOfLevelZeroTilesX: 4,
numberOfLevelZeroTilesY: 2
})
this._tilingScheme = Cesium.defaultValue(options.tilingScheme, myGeographicTilingScheme)
// mvt 解析库
this._mvtParser = new MVT()
/**
* 计算分辨率,这里是定义的 4326 或者 4490 的分辨率,基本上就是固定的
* for (var i = 0; i < 20; i++) {
* let reso = 180/(256*Math.pow(2, i));
* }
*/
this._resolutions = [0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125, 0.02197265625, 0.010986328125,
0.0054931640625, 0.00274658203125, 0.001373291015625, 0.0006866455078125, 0.00034332275390625, 0.000171661376953125,
0.0000858306884765625, 0.00004291534423828125, 0.000021457672119140625, 0.000010728836059570312, 0.000005364418029785156,
0.000002682209014892578, 0.000001341104507446289]

// 处理样式信息,默认传入的就是一个可以解析的样式函数
if(!options.styleConfig){
throw new Error("样式信息无效");
}
this._styleConfig=options.styleConfig;

// 瓦片请求点url
this._key = Cesium.defaultValue(options.key, "")
this._url = Cesium.defaultValue(options.url, "")
// 瓦片渲染队列
this._tileQueue = new Cesium.TileReplacementQueue()
this._cacheSize = 1000

// 这些东西有没有用,这个我暂时还没有搞明白
this._hasAlphaChannel = Cesium.defaultValue(options.hasAlphaChannel, true)
this._errorEvent = new Cesium.Event()
this._readyPromise = Cesium.defer()
this._credit = undefined
this._ready = true
}
// 定义 provider 的属性,这里有什么用,其实就是参考的官方定义一个 provider 所需要定义的一些属性进行的编写
Object.defineProperties(MvtImageryProvider.prototype, {
proxy: {
get: function () {
return undefined
}
},

tileWidth: {
get: function () {
return this._tileWidth
}
},

tileHeight: {
get: function () {
return this._tileHeight
}
},

maximumLevel: {
get: function () {
return this._maximumLevel
}
},

minimumLevel: {
get: function () {
return this._minimumLevel
}
},

tilingScheme: {
get: function () {
return this._tilingScheme
}
},

rectangle: {
get: function () {
return this._tilingScheme.rectangle
}
},

tileDiscardPolicy: {
get: function () {
return undefined
}
},

errorEvent: {
get: function () {
return this._errorEvent
}
},

ready: {
get: function () {
return this._ready
}
},

readyPromise: {
get: function () {
return this._readyPromise.promise
}
},

credit: {
get: function () {
return this._credit
}
},

hasAlphaChannel: {
get: function () {
return this._hasAlphaChannel
}
}
})
/**
*
* @param {*} x
* @param {*} y
* @param {*} level
* @returns
*/
MvtImageryProvider.prototype.getTileCredits = function (x, y, level) {
return undefined
}
/**
* 矢量数据选中
* @param {*} x
* @param {*} y
* @param {*} level
* @param {*} longitude
* @param {*} latitude
* @returns
*/
MvtImageryProvider.prototype.pickFeatures = function (x, y, level, longitude, latitude) {
return undefined
}
/**
* 获取矢量瓦片并渲染
* @param {*} x
* @param {*} y
* @param {*} level
* @param {*} request
* @returns
*/
MvtImageryProvider.prototype.requestImage = function (x, y, level, request) {
let cacheTile = findTileInQueue(x, y, level, this._tileQueue)
if (cacheTile != undefined) {
return new Promise((resolve, reject) => {
resolve(cacheTile)
})
} else {
let that = this
let url = this._url
// 这里不知道为什么,如果直接写level的话,那么我的4326的坐标系,就无法获取正确的图层
level=level+2

let 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)

let resource = Cesium.Resource.createIfNeeded(url)
return resource.fetchArrayBuffer().then((arrayBuffer) => {
try {
let canvas = document.createElement("canvas")
// 这里的width为什么是 4096,暂时没有理论支撑,不太清楚。
// 而且这里设置 4096 x 4096 特别的影响性能,这个问题在下一篇文章中会进行说明和修复
canvas.width = 4096
canvas.height = 4096
let ctx = canvas.getContext("2d")
let render = toContext(ctx);
// 解析mvt数据
let features = that._mvtParser.readFeatures(arrayBuffer)
// 获取样式信息
let styleConfig = that._styleConfig
// 遍历解析后的feature,将feature渲染到canvas上
for (let i = 0; i < features.length; i++) {
let feature = features[i];
let properties = feature.getProperties()
let featureLayer = properties.layer
let styleFunc = styleConfig[featureLayer] ? styleConfig[featureLayer].styleFunc : ""

// 获取样式信息
if(styleFunc){
let styles = styleFunc(features[i],this._resolutions[level])
if(styles){
// 循环遍历渲染feature
for (let j = 0; j < styles.length; j++) {
render.drawFeature(feature,styles[j]);
}
}
}
}
// 清理内存
features = null
render = null

// 渲染缓存队列(有没有用,还待考察)
if (that._tileQueue.count > that._cacheSize) {
trimTiles(that._tileQueue, that._cacheSize / 2)
}
// 切片缓存队列渲染(有没有用,还待考察)
canvas.xMvt = x
canvas.yMvt = y
canvas.zMvt = level
that._tileQueue.markTileRendered(canvas)
// 返回待渲染的 canvas
return canvas
} catch (error) {
console.log(error)
}

})
}
}
/**
* 查找缓存切片是否存在
* @param {*} x
* @param {*} y
* @param {*} level
* @param {*} tileQueue
* @returns
*/
function findTileInQueue(x, y, level, tileQueue) {
let item = tileQueue.head
while (item != undefined && !(item.xMvt == x && item.yMvt == y && item.zMvt == level)) {
item = item.replacementNext
}
return item
}
/**
* 移除缓存切片
* @param {*} tileReplacementQueue
* @param {*} item
*/
function removeQueue(tileReplacementQueue, item) {
let previous = item.replacementPrevious
let next = item.replacementNext

if (item === tileReplacementQueue._lastBeforeStartOfFrame) {
tileReplacementQueue._lastBeforeStartOfFrame = next
}

if (item === tileReplacementQueue.head) {
tileReplacementQueue.head = next
} else {
previous.replacementNext = next
}

if (item === tileReplacementQueue.tail) {
tileReplacementQueue.tail = previous
} else {
next.replacementPrevious = previous
}

item.replacementPrevious = undefined
item.replacementNext = undefined

--tileReplacementQueue.count
}

/**
*
* @param {*} tileQueue
* @param {*} maximumTiles
*/
function trimTiles(tileQueue, maximumTiles) {
let tileToTrim = tileQueue.tail
while (tileQueue.count > maximumTiles && Cesium.defined(tileToTrim)) {
let previous = tileToTrim.replacementPrevious

removeQueue(tileQueue, tileToTrim)
// delete tileToTrim
tileToTrim = null

tileToTrim = previous
}
}
// 导出 Provider
export default MvtImageryProvider;

3.图层加载

因为我才用的是mars3d的库,所以做了一些封装,主要就是处理获取到的 mapbox style 的 json 文件。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import * as mars3d from "mars3d";
import MvtImageryProvider from "./MvtImageryProvider.js";
import {stylefunction} from 'ol-mapbox-style';
import VectorTileLayer from 'ol/layer/VectorTile.js';

export default class VectorLayer {
constructor(options){
this.map = window.map
this.options = options || {}
// 获取样式信息,并进行解析
this._styleUrl = options.styleUrl
this.getStyle(this._styleUrl)
}

/**
* 加载样式信息
*/
getStyle(styleUrl){
styleUrl = styleUrl||this._styleUrl
mars3d.Util.fetchJson({
url: styleUrl,
queryParameters: {
access_token: this.options.key || "mars3d"
}
})
.then((styleJson) => {
// this.options.styleConfig = json
console.log(styleJson)

// 临时变量
const olLayer = new VectorTileLayer(); // 临时 openlayer 图层
const resolutions = [0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125, 0.02197265625, 0.010986328125,
0.0054931640625, 0.00274658203125, 0.001373291015625, 0.0006866455078125, 0.00034332275390625, 0.000171661376953125,
0.0000858306884765625, 0.00004291534423828125, 0.000021457672119140625, 0.000010728836059570312, 0.000005364418029785156,
0.000002682209014892578, 0.000001341104507446289]

// 遍历全部的图层,确定图层最大最小显示级别
let layers = styleJson.layers
let layersCount = layers.length
let layerConfig = {}
for(let i = 0;i < layersCount; i++){
let layer = layers[i]
// 去掉背景和栅格图层
let type = layer.type
if(type === "background" || type === "raster" ) {
continue
}
let layerid = layer.id
layer.index = i // 添加数组索引,方便快速定位
layerConfig[layerid] = layer
}

let sourceConfig = {} // 数据源配置
for(let layerid in layerConfig){
let layer = layerConfig[layerid]
// 如果是带 ref 属性,说明是一个引用,具体的内容需要在另外一个图层中获取
let ref = layer.ref
if(ref){
continue
let targetSource = layerConfig[ref]
let sourcIindex = layer.index
layer = Object.assign(targetSource,layer)
// 覆盖原始样式,这样就不会在处理时出现错误
styleJson.layers[sourcIindex]=layer
}

let sourceName = layer.source
let source = sourceConfig[sourceName]
if(!source){
source = {}
}
// 最大最小级别
let minzoom = layer.minzoom || 1
let maxzoom = layer.maxzoom || 20
if(source.minzoom >= minzoom) {
source.minzoom = minzoom
}
if(source.maxzoom <= maxzoom) {
source.maxzoom = maxzoom
}
// 创建样式,因为 styleFunction 需要一个 openlayer 图层作为载体,于是就创建了一个临时的图层
const styleFunc = stylefunction(olLayer, styleJson, sourceName, resolutions);
let styleConfig = source.styleConfig
if(!styleConfig) {
styleConfig = {}
}
let layerId = layer.id
let sourceLayer = layer["source-layer"]
styleConfig[sourceLayer] = {
id: layerId,
sourceLayer: sourceLayer,
styleFunc: styleFunc
}

// 保存引用
source.styleConfig = styleConfig

// 保存引用
sourceConfig[sourceName] = source
}

// 获取全部的 souurces ,获取需要渲染的 类型为 vector 的图层,并创建
let sources = styleJson.sources
for(let sourceName in sourceConfig){
let sourceLayer = sourceConfig[sourceName] // 处理后配置
let originSource = sources[sourceName] // 元数据配置
let styleConfig = sourceLayer.styleConfig
if(originSource.type == "vector") {
// 切片地址,这里其实是一个数组,但是我暂时没有进行处理
const tiles = originSource.tiles
const url = tiles[0]
const minimumLevel = sourceLayer.minimumLevel || 1
const maximumLevel = sourceLayer.maximumLevel || 20
// 创建图层
const provider = new MvtImageryProvider({
url: url,
styleConfig: styleConfig,
minimumLevel: minimumLevel,
maximumLevel: maximumLevel
})
let cesium = mars3d.Cesium
const viewer = this.map.viewer
//通过imageryLayers获取图层列表集合
const layers = viewer.scene.imageryLayers;
layers.addImageryProvider(provider);
}
}

/**
* 测试
*/
console.log(sourceConfig)
// 或者使用 mars3d 的重载的方法进行 mvt 的加载
const mvtLayer = new mars3d.layer.MvtLayer({
url: "https://map.hongjing.fpi-inc.site:8081/maps/tdt_jj/{z}/{x}/{y}.mvt",
styleConfig: sourceConfig["osm_jj"].styleConfig,
minimumLevel: 4,
maximumLevel: 20
});
this.map.addLayer(mvtLayer);


})
.catch(function (error) {
console.log("加载样式出错", error)
})
}
}

/**
* 扩展mars3d 的 BaseTileLayer
*/
class MvtLayer extends mars3d.layer.BaseTileLayer {
//构建ImageryProvider
_createImageryProvider(options) {
return createImageryProvider(options)
}
}
function createImageryProvider(options) {
return new MvtImageryProvider(options)
}
MvtLayer.createImageryProvider = createImageryProvider

// 在 mars3d 中进行注册
const layerType = "mvt" //图层类型
mars3d.LayerUtil.register(layerType, MvtLayer)
mars3d.LayerUtil.registerImageryProvider(layerType, createImageryProvider)

//对外接口
mars3d.provider.MvtImageryProvider = MvtImageryProvider
mars3d.layer.MvtLayer = MvtLayer

4.性能优化

虽然我使用了上面的代码,实现了图层的加载和显示,但是在使用面的代码进行创建的时候,发现内存占用非常的大。浏览器会不断的申请内存,直到把内存撑爆。后来我发现了是因为这个 canvas 的大小设置为4096 过大了。针对返回的数据是 4096 的像素坐标,这里我觉得有两种解决方案,一种就是对feature的坐标进行转换,将其转为0到4096范围内,另外一种就是将 canvas 进行适当的缩放。这两种到底哪种性能好。我觉得性能都不好,最好就是在数据请求的时候,就已经修改好了。

1.坐标转换

这里涉及到一个概念,就是说 mvt 的 extent 通常为4096,但是并不是说非要渲染为4096的坐标系,而是渲染为 256 x 256 的瓦片。这个其实就是一个矩阵相成的例子,将一个数组,转换成另外一个数组,所以才会有 this._transform = [0.125, 0, 0, 0.125, 0, 0] 这段代码,其实这段代码就是在瓦片是 512x512 的时候,如何从 4096x4096 转换成 512x512 的转换参数。

2.canvas 画布缩放

这种思路就是我尝试进行坐标的缩放,结果失败了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 /**
* 测试方法
*/
ctx.scale(0.0625, 0.0625);
let canvasData = ctx.getImageData(0, 0,4096, 4096); // 保存画布
console.log(canvasData)
let recanvas = document.createElement("canvas")
recanvas.width = 256;
recanvas.height = 256;
let rectx = recanvas.getContext("2d")
rectx.putImageData(canvasData, 256, 256);
canvas.width = 256;
canvas.height = 256;
ctx.putImageData(canvasData, 200, 200);
canvas=recanvas
canvas.width=256;
canvas.height=256;
ctx.putImageData(canvasData,0,0,0,0,256,256)
清理画布
recanvas = null

5.RenderFeature

在使用openlayer进行数据解析的时候,进行读取的时候实际上转换成的 feature,不是。

小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 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.
幸福是年华的沉淀,微笑是寂寞的悲伤。