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


    思考

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

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

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

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

    从后面的解释我们可以看明白,其实这两个参数控制的是该图层支持的最小和最大的 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    然后我们再加载一张 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是现在这个做法有个问题是,底下的 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);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这样一改,你就会发现,在后端不需要调整的情况下,就能达到想要的效果了。

    如果觉得,以上一些文字讲解的不是很直观,可以狠戳下面的 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
      })
    });
    
    
    • 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

    从上面代码中我们能发现,在 openlayers 中,我们用了更优雅的代码,就达到了和 cesium 中类似的效果。

    如果觉得,上面的文字讲解不是很直观,可以狠戳下面的 demo,亲自体验:

    CodePen - showLayerFromSpecifiedLevelByOL

    后记

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

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

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

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

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

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

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

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

    凡事预则立,不预则废。

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

  • 相关阅读:
    动态规划-罪犯转移问题
    Gzip压缩
    财务福音!用Python+OCR人工智能识别发票自动存入Excel表格保姆级教程
    Building a Robust Data Infrastructure for Cloud Computing Platforms
    win10系统单独编译和使用WebRTC的回声消除(AEC)、音频增益(AGC)、去噪(NS)模块
    腾讯云CVM服务器操作系统镜像大全
    CEPH 系统盘挂了,如何使用数据盘恢复
    什么是服务器节点?
    一个有效的图表图像数据提取框架
    解码未来:Web3如何重塑我们的生活
  • 原文地址:https://blog.csdn.net/lovefengruoqing/article/details/126513993