OpenLayers使用WMS加载GeoTiff数据源踩坑记录

前言

OpenLayers最近版本(6.11.0)上看到了使用WebGLTile图层加载GeoTIFF的示例,功能强大,不仅可以在前端直接显示tif影像,还可以做分波段彩色合成,对比度拉伸等色彩上的调整。简单试了下,发现数据源上存在一定的限制
在这里插入图片描述
WebGLTile的source属性接受DataTileSource和TileImage两种source类型,结合示例里的数据源发现WebGLTile图层只接受单张tif或者XYZ切片格式数据源,目前还不支持WMS地图服务,虽然GeoServer的WMS服务支持输出image/tiff。
这时看着 #GeoTIFF tile pyramid这个示例的我灵光一闪,是不是可以正常地用TileWMS加载发布为WMS的影像,设置输出tif格式,同时设置 #tileLoadFunction截胡即将加载的瓦片,将tif瓦片用GeoTIFF tile pyramid里的方法加载到地图上,如下

  1. 构建map和source
// 设置瓦片格网
const tileGrid = new TileGrid({
  origin: [-180, 90],
  resolutions: [0.703125, 0.3515625, 0.17578125, 8.7890625e-2, 4.39453125e-2],
  tileSizes: [
    [512, 256],
    [1024, 512],
    [2048, 1024],
    [4096, 2048],
    [4096, 4096]
  ]
});
// 一些全局对象
const pyramid = new LayerGroup();
const layerForUrl: any = {};
const zs = tileGrid.getResolutions().length;
// 构建source
const source = new TileWMS({
  url: 'http://localhost:8081/geoserver/taihu/wms',
  params: {
    FORMAT: 'image/tiff',
    VERSION: '1.1.1',
    tiled: true,
    STYLES: '',
    LAYERS: 'workspace:layername'
  },
  tileLoadFunction: (imageTile, src) => {
  	// 这里截胡瓦片
    useLayer(src, imageTile.tileCoord);
  }
});
// 构建图层和map
const layer = new Layers.Tile({
 source: source
});
this.map = new Map({
  target: 'map',
  layers: [layer, pyramid],
  view: new View({
    projection: 'EPSG:4326',
    center: [0, 0],
    zoom: 0,
    showFullExtent: true
  })
});
  1. 根据瓦片请求地址和xyz构建webgl图层,添加到图层组里
function useLayer(url: string, zxy: number[]) {
  const [z, x, y] = zxy;
  // 防止重复
  if (!(url in layerForUrl)) {
    const options: any = {
      sources: [
        {
          url: url
        }
      ]
    };
    const source = new GeoTIFF(options);
	// 构建webgl图层
    const layer = new WebGLTileLayer({
      minZoom: z,
      maxZoom: z === 0 || z === zs - 1 ? undefined : z + 1,
      style: {
        color: [
          'array',
          ['/', ['band', 1], 2000],
          ['/', ['band', 2], 2000],
          ['/', ['band', 3], 2000],
          1
        ]
      },
      extent: tileGrid.getTileCoordExtent([z, x, y]),
      source
    });

    pyramid.getLayers().push(layer);

    layerForUrl[url] = true;
  }
}

想法很美好,但是行不通

在这里插入图片描述
于是漫长的爬坑(失败的尝试)开始了。

坑1不使用WebGL图层,自己用GeoTiff.js加载

看了下源码,OL用GeoTiff.js解析tif数据源,我觉得我也行(如此普通却这么自信)

这里简单介绍下GeoTiff.js,这东西可以解析多种格式的tif数据,按照官网例子然后可以用 #plotty将解析出来的数据渲染为canvas

美好想法:还是用TileWMS加载WMS,设置tif格式,用tileLoadFunction截胡,用GeoTiff.js解析,用plotty渲染到canvas上,再用 canvas.toDataURL(“image/png”)转base64,重新回传到imageTile的image里。核心代码如下

const source = new TileWMS({
  url: "http://localhost:8081/geoserver/taihu/wms",
  params: {
    FORMAT: "image/tiff",
    VERSION: "1.1.1",
    tiled: true,
    STYLES: "",
    LAYERS: "workspace:layername",
  },
  tileLoadFunction: async (imageTile, src) => {
    // 按照geotiffjs官网示例处理
    const tiff = await fromUrl(src);
    const image = await tiff.getImage();
    const data = await image.readRasters();

    // imageTile没有直接暴露image
    const writeImage = (imageTile as any).image_;
    // 一个工具canvas
    const canvas = document.createElement("canvas");
    // 也是从官网上抄的
    const plot = new plotty.plot({
      canvas,
      data: data[0],
      width: image.getWidth(),
      height: image.getHeight(),
      domain: [0, 256],
      colorScale: "viridis",
    });
    plot.render();
    // 传回去
    writeImage.src = canvas.toDataURL("image/png");
  },
});

结果还是报错request failed,不服,既然是请求的错误那就不用url了,改造了一下

      tileLoadFunction: async (imageTile, src) => {
        // 按照geotiffjs官网示例处理
        const response = await fetch(src);
        const arrayBuffer = await response.arrayBuffer();
        const tiff = await fromArrayBuffer(arrayBuffer);
        const image = await tiff.getImage();
        const data = await image.readRasters();

		...下边不变
      },

结果这个样
在这里插入图片描述
em 总算是出来了,就是色彩渲染需要调整一下,毕竟不能像WebGLTileLayer那样用style去设置渲染了,问题不大



个锤子,plotty虽然有colorScale这个属性能设置渲染样式,但是并没有WebGLTileLayer那种可以根据波段渲染的方法,只有几种样式预设和一个不明觉厉的addColorScale方法
在这里插入图片描述
总之简单看了一下,geotiffjs的readRasters方法解析出来的是一堆应该是包含颜色值的矩阵数组,plotty使用WebGL把它渲染出来,而我对颜色矩阵无从下手,对WebGL也一脸懵,遂暂时放弃。

坑2 debug所谓的Request failed

尝试失败之后决定着手解决一下最一开始出现的这个Request failed
中间过程不表,总之把这个直接问题解决了
在这里插入图片描述
然后不报错了,但是地图一片空白,我的大脑也是。
遂放弃

坑3 不截胡了,直接构造WMS请求,转换返回的tif

感觉问题可能出现在两个方面,瓦片请求不对或者渲染有问题。
对Ol的地图渲染不熟悉,简单顺了一下,又遇到了WebGL,感觉太深了,还是返璞归真,改造一下GeoTIFF tile pyramid例子。
例子里是监听地图moveend,用tileGrid里获取的xyz构造瓦片地图地址

const url = `https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/${z}/${y}/${x}.tif`;

我应该也可以通过tileGrid构造bbox,作为参数直接请求WMS瓦片,而不是用TileWMS类去请求

// 根据bbox构造请求
function getUrlFromExt(BBOX: number[]) {
  const url = `http://localhost:8081/geoserver/taihu/wms?`;
  const urlparam = new URLSearchParams();
  const params: any = {
    SERVICE: 'WMS',
    VERSION: '1.1.1',
    REQUEST: 'GetMap',
    FORMAT: 'image/tiff',
    TRANSPARENT: true,
    tiled: true,
    STYLES: '',
    LAYERS: 'workspace:layername',
    WIDTH: 256,
    HEIGHT: 256,
    SRS: 'EPSG:4326',
    BBOX
  };
  for (const key in params) {
    if (Object.prototype.hasOwnProperty.call(params, key)) {
      const element = params[key];
      urlparam.append(key, element);
    }
  }
  return url + urlparam.toString();
}
// 根据边界生成url,剩下的和例子里差不多一样
function useLayer2(ext: number[], zxy: number[]) {
  const [z, x, y] = zxy;
  const url = getUrlFromExt(ext);
  if (!(url in layerForUrl)) {
    const options: any = {
      sources: [
        {
          url: url
        }
      ]
    };
    const source = new GeoTIFF(options);

    const layer = new WebGLTileLayer({
      minZoom: z,
      maxZoom: z === 0 || z === zs - 1 ? undefined : z + 1,
      style: {
        color: [
          'array',
          ['/', ['band', 1], 2000],
          ['/', ['band', 2], 2000],
          ['/', ['band', 3], 2000],
          1
        ]
      },
      extent: tileGrid.getTileCoordExtent([z, x, y]),
      source
    });

    pyramid.getLayers().push(layer);

    layerForUrl[url] = true;
  }
}
// 地图里只保留pyramid
const map = new Map({
  target: 'map',
  layers: [pyramid],
  view: new View({
    projection: 'EPSG:4326',
    center: [0, 0],
    zoom: 0,
    showFullExtent: true
  })
});
// 除了xyz,还需要传extent
const ext = getIntersection(
  [-180, -90, 180, 90],
  map.getView().calculateExtent()
);
useLayer2(ext, [0, 0, 0]);
map.on('moveend', () => {
  const view = map.getView();
  const ext = getIntersection([-180, -90, 180, 90], view.calculateExtent());
  tileGrid.forEachTileCoord(
    ext,
    tileGrid.getZForResolution(view.getResolution()!),
    ([z, x, y]) => useLayer2(ext, [z, x, y])
  );
});

结果如图
在这里插入图片描述
在这里插入图片描述

那个黑色的小块块是可以拖动放大的,而且拖动时浏览器会请求WMS
目前还在这个坑里。

脱坑

搞了半天是数据源不对,不能直接用普通的tif文件,需要转成COG(Cloud Optimized GeoTIFF)。转好之后可以像示例那样直接加载。转换用的是GDAL,方法上边那个链接里有。
问题依然存在,首先是样式设置,为了较好的显示效果需要设置波段拉伸(波段拉伸的示例见#Rendering 16-bit NumpyTiles
,而前端目前没有找到有效的方法获取COG各波段的范围。测试中我是先在QGIS里打开数据文件获取波段范围再手动写死在代码里(下图),日后肯定不行。
在这里插入图片描述

还有个问题是数据加载到地图上有个黑框,这个问题很奇怪我以为是OL的bug于是在git上提了issue(#WebGLTile layer rendered error images outside original extent),回复说图层范围相交的瓦片不会被裁切,所以问题应该出在生成tif生成COG的瓦片设置上,这块还不熟悉,需要继续研究。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值