相关文章推荐

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.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

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

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

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

  •  
    推荐文章