想必深入用过 cesium 的小伙伴都知道,cesium 可以支持加载很多不同类型的影像服务,呈现渲染在三维地球上。
但是 cesium 有个不太好的地方是,其提供的接口,不直接支持从指定某一层级开始加载影像图。
上面这句话,可能会有一丝丝的歧义。
比如就拿我们常见的加载 wmts 或者 wms 影像服务的接口来举例,官方提供的接口中提供了两个参数 minimumLevel
和 maximumLevel
从后面的解释我们可以看明白,其实这两个参数控制的是该图层支持的最小和最大的 level-of-detail,翻译过来应该就是详细程度,而 level 本身又指代不同的层级,不同的层级与详细程度是密不可分的。
因此,我们应该明白了,官方提供的接口,是在某个图层加载的时候,从第几层开始加载,到第几层为止。
有的童鞋可能要问了,这会导致什么问题呢?
我们知道,不同的视角下,浏览器窗口对应的实际地理区域大小是不一样的。每个瓦片,其实大小都是一样的,大部分都是 256x256 或者 512x512,但是其指代的实际地理区域大小是跟其层级有关的。
就拿我们常用的 wmts 服务为例,一般在 0 层的时候,会划分成 2 张瓦片,分别是 (0, 0)、(0, 1),换算成实际区域,每张瓦片表示的范围大概是半个采用墨卡托投影铺开来的地球。(当然,这里只以笔者经常碰到的情况作为示例,如果有别的层级划分方法,可做类比)
在 1 层的时候,其实就是把 0 层划分出来的瓦片,通过四叉树的方式,进一步划分。将 0 级的 (0, 0),划分为 1 级的 (0,0)、(0,1)、(1,0)、(1,1),每张瓦片所表示的范围,只有 0 层的瓦片的四分之一。然后随着层级的变大,依次将每张瓦片,向下进一步细分。
其实明白了,四叉树的原理后,就能搞明白了瓦片的划分方式和原理了。
明白了上述原理以后,我们就明白了,每一层的瓦片数量,几乎是呈现指数级上升的趋势。
基于地图瓦片的划分特点,正常情况下,贴图算法,会为每个视角自动匹配最合适的贴图层级,以使得影像呈现最佳的浏览效果、同时也节约了宝贵的带宽和系统内存。
如果你强行从 1 级开始加载影像,默认情况下,在全球视角下,就需要加载 8 张瓦片才能覆盖全球。
从 2 级开始加载影像,默认情况下,在全球视角下,就需要加载 32 张瓦片才能覆盖全球。
从 3 级开始加载影像,默认情况下,在全球视角下,就需要加载 128 张影像才能覆盖全球。
再算下去,我们就能明白,为什么不能将 minimumLevel
参数设置的太高了,设置的太高,会导致默认在全球视角,会加载很多最低一层级的瓦片。
现在可以解释,为何在文章开头的时候,我会说,“从指定层级开始显示某一张影像”这句话是有歧义的。
更准确的说,我们希望的效果是,当地图应用上需要加载低于指定最低层级的瓦片时候,全部贴上透明的瓦片;当需要加载高于指定最高层级的瓦片的时候,不再请求更高层级的瓦片,用最高层级的瓦片放大代替。
这里其实可以思考一下,为什么最高层级可以采取放大的方式代替,而低层级不行呢?
答案显而易见,低层级瓦片放大了看,只不过会呈现马赛克效果,并没有任何额外的成本支出。
而将低层级瓦片,贴在高层级上,是会需要额外的开销,这正是瓦片地图的精髓。
有的童鞋可能会问了,什么情况下,需要这种应用场景呢?
对于矢量图而言,自然是没有该需求的,但是对于栅格图而言,这种应用场景可就太有必要了。
对于栅格图而言,放在 100% 的缩放比下看,效果才是最好的。
无论是放大看,还是缩小看,我们都需要对图像进行重采样。
所以如果我们制作一副某个区域的影像图,拿不同分辨率的影像,放在对应的层级,最终构成一张影像金字塔,效果才是最自然的。
我们知道,卫星拍摄的影像图,有不同的分辨率。而且一般情况下,对于一副影像图而言,分辨率越高,表示的范围会越小。
这个道理应该很好理解。
假设我们有一张 1m 分辨率的影像,差不多等同于,影像图中的一个像素点,就表示地理上的 1m x 1m 大小的范围,该幅图像表示的范围越大,就需要由越多的像素点组成。
所以,基于这个道理,我们做一张全球范围 16m 分辨率的影像图也许很容易,但是想做一张全球范围 1m 甚至于亚米的影像图就很难了。
假设我们需要做一张全球范围的影像图,只能对重点区域应用高分辨率影像,非重点区域应用低分辨率影像,这样互相搭配着使用,才比较符合我们实际的应用场景。
那么转换成我们实际的 webgis 应用来说,比较合理的使用方式就出来了。
我们用 16m 分辨率的影像当全球的底图,放在最下面,这个图只会加载 0-10 级;全国范围我们用 8m 分辨率的影像当底图,这张图只会在 11-13 级的时候会展示;比如我们关心江苏省,那么江苏省我们会采用 2m 分辨率的影像当底图,这张图只会在 14-15 级的时候展示。
按照这种构造方式,我们就能够造出一张效果好,并且不浪费带宽和电脑资源的 webgis 应用的底图。
既然一开始我们拿 cesium 来举例,那我们就先聊聊,用 cesium 构建我们的 webgis 应用的时候,应该如何实现从指定层级开始显示某一张影像图。
就如前面所说,cesium 没提供接口,让我们直接实现这种效果。
所以我们必须想办法在不影响框架核心代码的前提下,做一些修改,从而能解决我们面对的问题。
假设现在我们有两份图源:
一份是 cesium 自带的一个测试影像数据集:cesium/Source/Assets/Textures/NaturalEarthII at main · CesiumGS/cesium · GitHub
一份来自 usgs 官网提供的影像数据集:USGSImageryOnly (MapServer)
前一份数据,只有 0-2 级,后一份数据有 0-8 级,为了模拟我们上面说的效果,我们作出如下规定:
当需要加载 0-2 级瓦片的时候,我们采用 cesium 自带的 NaturalEarthII 数据集;当需要加载 2-8 级的影像的时候,我们采用 usgs 官网提供的影像数据集。
做成以下图示的效果:
为了实现这种效果,我们需要同时在球上加载两张影像底图。
为了简单起见,我们先加载 NaturalEarthII 的数据集作为底图。
let wrapper = document.querySelector("#cesiumContainer");
let config = {
imageryProvider: new Cesium.TileMapServiceImageryProvider({
url: Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
}),
navigationInstructionsInitiallyVisible: false,
projectionPicker: false,
creditContainer: null,
animation: false, // 是否创建动画小器件,左下角仪表
baseLayerPicker: false, // 是否显示图层选择器
fullscreenButton: false, // 是否显示全屏按钮
geocoder: false, // 是否显示geocoder小器件,右上角查询按钮
homeButton: false, // 是否显示Home按钮
infoBox: false, // 是否显示信息框
sceneModePicker: true, // 是否显示3D/2D选择器
selectionIndicator: false, // 是否显示选取指示器组件
timeline: false, // 是否显示时间轴
navigationHelpButton: false, // 是否显示右上角的帮助按钮
requestRenderMode: true, // 是否采用请求渲染模式
};
const viewer = new Cesium.Viewer(wrapper, config);
然后我们再加载一张 USGS 提供的影像图,放在 NaturalEarthII 底图上面。
const shadedRelief1 = new Cesium.WebMapTileServiceImageryProvider({
url:
"https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/WMTS",
layer: "USGSImageryOnly",
style: "default",
format: "image/jpeg",
tileMatrixSetID: "default028mm",
maximumLevel: 8,
credit: new Cesium.Credit("U. S. Geological Survey")
});
viewer.imageryLayers.addImageryProvider(shadedRelief1);
但是现在这个做法有个问题是,底下的 NaturalEarthII 底图始终会被后加入的 USGS 这张影像图给盖住。
这并不是我们想要的效果。
为了达到我们上面图示的最终效果,我们考虑将 Cesium.ImageryProvider.loadImage
方法重载掉。
但是为了影像最小化,我们采取下面这种方式进行重载方式:
Cesium.ImageryProvider.loadImage2 = Cesium.ImageryProvider.loadImage;
Cesium.ImageryProvider.loadImage = function loadImage(imageryProvider, url) {
if (imageryProvider instanceof Cesium.WebMapTileServiceImageryProvider) {
if (
url.queryParameters.layer === "USGSImageryOnly" &&
parseInt(url.queryParameters.tilematrix, 10) < 3
) {
// 当经过重重判断,发现是我们不想显示对应层级的瓦片,我们直接返回一个透明的空白图片代替
return new Promise((resolve) => {
const img = new Image();
img.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
resolve(img);
});
}
}
// 否则,我们将执行原方法,远程加载对应的瓦片
return Cesium.ImageryProvider.loadImage2.call(this, imageryProvider, url);
};
这样一改,你就会发现,在后端不需要调整的情况下,就能达到想要的效果了。
如果觉得,以上一些文字讲解的不是很直观,可以狠戳下面的 demo,亲自体验:
CodePen - showLayerFromSpecifiedLevel
当然,以上写法,并不通用,只能当作 demo 来使用,思路仅供参考。如果你希望应用于项目之中,还需要继续完善,将逻辑写的更完善些。
虽然我们是以 cesium 的视角来切入这篇文章所谈论的知识点的,但是作为一个合格的 webgis 开发者,怎么能少了在 openlayers 中的应用呢!
在 openlayers 中,用起来相对而言,就更简单了。
因为 openlayers 每次加载图层的时候,支持自定义 tileLoadFunction
,简而言之,就是官方提供了接口,直接方便我们重载瓦片加载方法。
我们直接加载两张底图,前一张,我们通过配合 tileLoadFunction
方法,控制层级,高于 9 级我们才显示;后一张,我们通过设置 maxZoom
属性,让它最大只显示到 8 级,再往上就不显示。
let { Map, View, source, layer } = ol;
const map = new Map({
layers: [
new layer.Tile({
source: new source.OSM({
tileLoadFunction(imageTile, src) {
let urlPattern = new URLPattern(src);
let pathArr = urlPattern.pathname.split("/");
let len = pathArr.length;
if (parseInt(pathArr[len - 3], 10) > 8) {
imageTile.getImage().src = src;
} else {
imageTile.getImage().src = "";
}
}
})
}),
new layer.Tile({
source: new source.OGCMapTile({
url:
"https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad"
}),
maxZoom: 8
})
],
target: "map",
view: new View({
center: [13345578.341963194, 3754164.381651712],
zoom: 8
})
});
从上面代码中我们能发现,在 openlayers 中,我们用了更优雅的代码,就达到了和 cesium 中类似的效果。
如果觉得,上面的文字讲解不是很直观,可以狠戳下面的 demo,亲自体验:
CodePen - showLayerFromSpecifiedLevelByOL
用过天地图影像图的童鞋,应该知道,我们直接在前端通过逻辑构建的图层组,就类似于天地图影像底图这一张图的效果。
但是仔细思考一下,两者有何优劣呢?
在效果上,我们的方案无疑是更胜一筹的,因为我们可以保证,在全球的任何地方、缩放到任何层级下某个坐标点上都会存在一张瓦片地图,区别只是,如果该坐标点落在我们关心的重点区域,就会出现精度高的瓦片,否则,只是一张放大了的低精度的瓦片。
这种效果,单纯用天地图没法实现,用天地图,你会发现,你只要定位到国外,并且不断放大视图,会发现,出现大量的该区域没有影像的提示。
有的童鞋可能会问了,你这种做法,不会导致前端页面额外的内存开销么?同时存在多个图层,不会导致额外的瓦片请求么?
如果构造的图层组合合理的话,理论上来说,是不会出现这种令人困扰的情况的。
所以为了使我们的方案达到最优的效果,我们就需要提前规划好每个图层在哪个层级区间内显示,显示的范围有多大。
不得不感叹,很多时候,我们往往为了得到更好的效果,就需要提前花费更多的时间,做出更加合理的筹划,做更多的准备。
凡事预则立,不预则废。
机会往往是留给有准备的人的。