矢量切片(vector tile)是当前 WebGIS 较热技术,国内的高德、百度等在线地图都使用了矢量切片技术。相较于传统栅格切片,矢量切片好处很多。简单几点就是:轻量、客户端渲染、还可加密(栅格切片容易被爬取)。矢量切片格式一般有 GeoJSON、TopoJSON 、MVT (MapBox Vector Tile)、PBF。
下面是 Cesium 加载 MVT 矢量切片的代码,由于样式渲染使用的了 openlayers,所以首先需要安装 openlayers。
一、安装 openlayers
npm install openlayers
二、MVT 加载核心代码
```javascript
// mvtProvider.js
import * as Cesium from 'cesium'
import ol from 'openlayers/dist/ol-debug'
export function MVTProvider (options) {
options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT)
this._tilingScheme = Cesium.defined(options.tilingScheme) ? options.tilingScheme : new Cesium.WebMercatorTilingScheme({ ellipsoid: options.ellipsoid })
this._tileWidth = Cesium.defaultValue(options.tileWidth, 512)
this._tileHeight = Cesium.defaultValue(options.tileHeight, 512)
this._readyPromise = Cesium.when.resolve(true)
this._ol = ol
this._mvtParser = new this._ol.format.MVT()
this._styleFun = Cesium.defined(options.styleFun) ? options.styleFun : createMapboxStreetsV6Style
this._key = Cesium.defaultValue(options.key, '')
this._url = Cesium.defaultValue(options.url, 'https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/{z}/{x}/{y}.vector.pbf?access_token={k}')
var sw = this._tilingScheme._rectangleSouthwestInMeters
var ne = this._tilingScheme._rectangleNortheastInMeters
var mapExtent = [sw.x, sw.y, ne.x, ne.y]
this._resolutions = ol.tilegrid.resolutionsFromExtent(mapExtent, 22, this._tileWidth)
this._pixelRatio = 1
this._transform = [0.125, 0, 0, 0.125, 0, 0]
this._replays = ['Default', 'Image', 'Polygon', 'LineString', 'Text']
this._tileQueue = new Cesium.TileReplacementQueue()
this._cacheSize = 1000
}
Object.defineProperties(MVTProvider.prototype, {
proxy: {
get () {
return undefined
}
},
tileWidth: {
get () {
return this._tileWidth
}
},
tileHeight: {
get () {
return this._tileHeight
}
},
maximumLevel: {
get () {
return undefined
}
},
minimumLevel: {
get () {
return undefined
}
},
tilingScheme: {
get () {
return this._tilingScheme
}
},
rectangle: {
get () {
return this._tilingScheme.rectangle
}
},
tileDiscardPolicy: {
get () {
return undefined
}
},
errorEvent: {
get () {
return this._errorEvent
}
},
ready: {
get () {
return true
}
},
readyPromise: {
get () {
return this._readyPromise
}
},
credit: {
get () {
return undefined
}
},
hasAlphaChannel: {
get () {
return true
}
}
})
MVTProvider.prototype.getTileCredits = function () {
return undefined
}
function findTileInQueue (x, y, level, tileQueue) {
var item = tileQueue.head
while (item !== undefined && !(item.xMvt === x && item.yMvt === y && item.zMvt === level)) {
item = item.replacementNext
}
return item
}
function remove (tileReplacementQueue, item) {
var previous = item.replacementPrevious
var 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
}
function trimTiles (tileQueue, maximumTiles) {
var tileToTrim = tileQueue.tail
while (tileQueue.count > maximumTiles &&
Cesium.defined(tileToTrim)) {
var previous = tileToTrim.replacementPrevious
remove(tileQueue, tileToTrim)
tileToTrim = null
tileToTrim = previous
}
}
MVTProvider.prototype.requestImage = function (x, y, level) {
var cacheTile = findTileInQueue(x, y, level, this._tileQueue)
if (cacheTile !== undefined) {
return cacheTile
}
var that = this
var url = this._url
url = url.replace('{x}', x).replace('{y}', y).replace('{z}', level).replace('{k}', this._key);
(function (x, y, z) {
var resource = Cesium.Resource.createIfNeeded(url)
return resource.fetchArrayBuffer().then((arrayBuffer) => {
var canvas = document.createElement('canvas')
canvas.width = 512
canvas.height = 512
var vectorContext = canvas.getContext('2d')
var features = that._mvtParser.readFeatures(arrayBuffer)
var styleFun = that._styleFun()
var extent = [0, 0, 4096, 4096]
var _replayGroup = new ol.render.canvas.ReplayGroup(0, extent, 8, true, 100)
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)
}
}
_replayGroup.finish()
_replayGroup.replay(vectorContext, that._pixelRatio, that._transform, 0, {}, that._replays, true)
if (that._tileQueue.count > that._cacheSize) {
trimTiles(that._tileQueue, that._cacheSize / 2)
}
canvas.xMvt = x
canvas.yMvt = y
canvas.zMvt = z
that._tileQueue.markTileRendered(canvas)
_replayGroup = null
return canvas
}).otherwise(() => {
})
}(x, y, level))
}
MVTProvider.prototype.pickFeatures = function () {
return undefined
}
function createMapboxStreetsV6Style () {
var fill = new ol.style.Fill({ color: '' })
var stroke = new ol.style.Stroke({ color: '', width: 1 })
var polygon = new ol.style.Style({ fill })
var strokedPolygon = new ol.style.Style({ fill, stroke })
var line = new ol.style.Style({ stroke })
var text = new ol.style.Style({
text: new ol.style.Text({
text: '', fill, stroke
})
})
var iconCache = {}
function getIcon (iconName) {
var icon = iconCache[iconName]
if (!icon) {
icon = new ol.style.Style({
image: new ol.style.Icon({
src: https://cdn.rawgit.com/mapbox/maki/master/icons/${iconName}-15.svg,
imgSize: [15, 15]
})
})
iconCache[iconName] = icon
}
return icon
}
var styles = []
return function (feature, resolution) {
var length = 0
var layer = feature.get('layer')
var cls = feature.get('class')
var type = feature.get('type')
var scalerank = feature.get('scalerank')
var labelrank = feature.get('labelrank')
var adminLevel = feature.get('admin_level')
var maritime = feature.get('maritime')
var disputed = feature.get('disputed')
var maki = feature.get('maki')
var geom = feature.getGeometry().getType()
if (layer === 'landuse' && cls === 'park') {
fill.setColor('#d8e8c8')
styles[length++] = polygon
} else if (layer === 'landuse' && cls === 'cemetery') {
fill.setColor('#e0e4dd')
styles[length++] = polygon
} else if (layer === 'landuse' && cls === 'hospital') {
fill.setColor('#fde')
styles[length++] = polygon
} else if (layer === 'landuse' && cls === 'school') {
fill.setColor('#f0e8f8')
styles[length++] = polygon
} else if (layer === 'landuse' && cls === 'wood') {
fill.setColor('rgb(233,238,223)')
styles[length++] = polygon
} else if (layer === 'waterway' &&
cls !== 'river' && cls !== 'stream' && cls !== 'canal') {
stroke.setColor('#a0c8f0')