webgis —— 从指定层级开始显示某一张影像图

思考

想必深入用过 cesium 的小伙伴都知道,cesium 可以支持加载很多不同类型的影像服务,呈现渲染在三维地球上。

但是 cesium 有个不太好的地方是,其提供的接口,不直接支持从指定某一层级开始加载影像图。

上面这句话,可能会有一丝丝的歧义。

比如就拿我们常见的加载 wmts 或者 wms 影像服务的接口来举例,官方提供的接口中提供了两个参数 minimumLevelmaximumLevel

webgis —— 从指定层级开始显示某一张影像图_第1张图片

从后面的解释我们可以看明白,其实这两个参数控制的是该图层支持的最小和最大的 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 来举例,那我们就先聊聊,用 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 来使用,思路仅供参考。如果你希望应用于项目之中,还需要继续完善,将逻辑写的更完善些。

openlayers 使用方案

虽然我们是以 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

后记

用过天地图影像图的童鞋,应该知道,我们直接在前端通过逻辑构建的图层组,就类似于天地图影像底图这一张图的效果。

但是仔细思考一下,两者有何优劣呢?

在效果上,我们的方案无疑是更胜一筹的,因为我们可以保证,在全球的任何地方、缩放到任何层级下某个坐标点上都会存在一张瓦片地图,区别只是,如果该坐标点落在我们关心的重点区域,就会出现精度高的瓦片,否则,只是一张放大了的低精度的瓦片。

这种效果,单纯用天地图没法实现,用天地图,你会发现,你只要定位到国外,并且不断放大视图,会发现,出现大量的该区域没有影像的提示。

有的童鞋可能会问了,你这种做法,不会导致前端页面额外的内存开销么?同时存在多个图层,不会导致额外的瓦片请求么?

如果构造的图层组合合理的话,理论上来说,是不会出现这种令人困扰的情况的。

所以为了使我们的方案达到最优的效果,我们就需要提前规划好每个图层在哪个层级区间内显示,显示的范围有多大。

不得不感叹,很多时候,我们往往为了得到更好的效果,就需要提前花费更多的时间,做出更加合理的筹划,做更多的准备。

凡事预则立,不预则废。

机会往往是留给有准备的人的。

你可能感兴趣的:(前端,webgis,Javascript,cesium,webgis,gis,openlayers,wmts)