前言:
COG(Cloud Optimized GeoTIFF)本质上是一个GeoTIFF文件,但与常规的大文件GeoTIFF相比,它更加适合用于HTTP文件服务器进行数据发布。它不仅仅存储了影像中的原始像素,同时还按照特定的方式对这些像素进行组织,使得客户端可以通过HTTP GET range请求,拿到他们真正需要的影像内容。这种机制可以让感知COG的客户端完全在线处理数据,因为他们可以拿到真正所需要的GeoTIFF的内容,而非下载整个超大的GeoTIFF文件。(原文链接:https://blog.csdn.net/qq_36635746/article/details/121516908)
更多信息参考:COG(Cloud optimized GeoTIFF——云优化GeoTiff)简介与实践_cog tiff-CSDN博客
Openlayer加载COG文件:
Openlayers更新了WebGLTile图层和GeoTIFFSource数据源,加载非常简单,并且官网也有示例如下:Cloud Optimized GeoTIFF (COG)
import GeoTIFF from 'ol/source/GeoTIFF.js';
import Map from 'ol/Map.js';
import TileLayer from 'ol/layer/WebGLTile.js';
const source = new GeoTIFF({
sources: [
{
url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif',
},
],
});
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: source,
}),
],
view: source.getView(),
});
mapbox加载COG文件:
经过一番搜索发现各社区论坛都没有相关内容,mapbox官网也没有相关的文档和例子,经过一番寻觅决定使用mapbox自定义图层和自定义Source的方式尝试自己加载出来。
经过一番寻觅和同事的帮助,我们发现有一个JavaScript库可以在前端处理COG:
geotiff.js
官网:geotiff.js
这个库支持处理COG,可以通过它动态的传入四至来获取该四至下的tiff影像数据,所以只要在地图上每个瓦片绘制时获取到瓦片行列号,转换为tiff的四至通过geotiffjs来获取到影像数据,再转换为图片绘制到地图上的对应位置即可!
上代码,geotiffjs部分:
tiff = await fromUrl('https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif')//依旧使用openlayers示例中的COG数据
let data = await this.tiff.readRasters({
bbox,//这里传入要查询的四至
samples:[0],//这里传入要查询的波段信息
width: tileSize,//瓦片宽高
height: tileSize,//瓦片宽高
interleave: true
});
代码中返回的data就是我们需要的影像数据了,具体的readRasters方法的参数请翻阅官方文档。接下来要做的就是利用mapbox的自定义Source在瓦片渲染时获取到将要渲染的行列号,转换为四至来获取对应位置的tiff数据,并转换成图片渲染到地图上了。
注意:
1.返回的数据为数组格式,里边存储的数据与传入的波段信息相关,也与tiff数据的波段信息相关,如果tiff数据是多波段,是彩色的,那么samples可以传入[0,1,2],返回的数组会是每个像素的rgb,也就是说,每个像素点有三个值,分别是rgb三色。若数据为单波段,则samples只能传入[0],返回的数据长度也为像素点数量,每个像素点仅有一个值。这对后续的转换图片操作很重要。
2.需注意tiff数据的坐标系,传入的四至需要和tiff数据本来的坐标系相匹配,否则会报错
mapbox自定义Source和自定义Layer
查看mapbox源码时我发现mapbox自定义有两种方式符合我的需求,一种是customLayer,一种是customSource
在mapbox官方文档里有相关customLayer的文档和示例,但却没有customSource相关信息,但是在mapbox源码示例里边有相关这两种自定义方案的示例
目前还没有找到合适的customLayer加载COG的方案,所以以下只写customSource加载COG的方案
customSource加载自定义图片
const CogSource={
type: "custom",//自定义图层的type需要传custom
loadTile({ z, x, y }) {//图层的type是raster时,每个瓦片加载都会触发该方法,接下来获取到行列号
console.log(x,y,z,'行列号')
//这里根据行列号获取到四至,通过四至去取tiff瓦片数据
//一系列转换操作最终把tiff瓦片数据转成图片绘制到canvas上面
const canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
return canvas;//返回一个自己绘制的canvas,mapbox会自己将图片绘制到对应位置
}
}
map.addSource("custom-source", CogSource);
map.addLayer({
id: "custom-source",
type: "raster",
source: "custom-source"
});
将二者结合,即可完美的把COG数据加载到mapbox地图上了。以下是全部代码:
页面部分使用vue3:
<template>
<div style="width: 100vw; height: 100vh" id="map"></div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { CogSource } from './CogSource'
let map: any = null;
onMounted(async () => {
const url = "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif";//数据在北非(3857)
mapboxgl.accessToken = "";
map = new mapboxgl.Map({
container: "map",
// style: mapImgStyle,
center: [116.4, 39.9],
zoom: 9
});
const customSource = new CogSource({
tiffUrl:url,
tileSize:256
});
map.on("load", () => {
map.addSource("custom-source", customSource);
map.addLayer({
id: "custom-source",
type: "raster",
source: "custom-source"
});
});
});
</script>
js部分:
import { fromUrl } from "geotiff"
function merc(x, y, z){
// 参考: https://qiita.com/MALORGIS/items/1a9114dd090e5b891bf7
const GEO_R = 6378137;
const orgX = -1 * ((2 * GEO_R * Math.PI) / 2);
const orgY = (2 * GEO_R * Math.PI) / 2;
const unit = (2 * GEO_R * Math.PI) / Math.pow(2, z);
const minx = orgX + x * unit;
const maxx = orgX + (x + 1) * unit;
const miny = orgY - (y + 1) * unit;
const maxy = orgY - y * unit;
return [minx, miny, maxx, maxy];
}
class CogSource{
tiffUrl = ""//用于存放tiff的路径
tiff = null//用于存放tiff实体
tiffImage = null//用于存放tiff实体的Image信息
tileSize = 256//用于存放瓦片尺寸
samples = null//用于存放波段数
type = "custom"
constructor({tiffUrl,tileSize}) {
this.tiffUrl = tiffUrl;
this.tiff = null;
this.type = "custom";
this.cache = new Map();
this.tileSize = tileSize;
}
async loadTile({ z, x, y }) {
if(!this.tiff){//若tiff还未加载则先加载tiff(这里可能存在Promise还未兑现就又重新执行的风险,考虑添加Promise并发锁)
this.tiff = await fromUrl(this.tiffUrl)
this.tiffImage = await this.tiff.getImage()
this.samples = this.tiffImage.getSamplesPerPixel();
}
let bbox = merc(x, y, z);//仅支持3857若tiff数据为4490则需要进行转换
let data = await this.tiff.readRasters({
bbox,
samples: this.samples==3?[0, 1, 2]:[0],//进行波段判断
width: this.tileSize,
height: this.tileSize,
interleave: true
});
let rgbadata = new Uint8ClampedArray(this.tileSize * this.tileSize * 4);
if(this.samples==3){
//data数据为rgb三个一组,但是rgbadata为rgba四个一组(这里可继续拓展功能,根据用户传入的参数调整rgba)
for (let i = 0; i * 3 < data.length; i++) {
rgbadata[i * 4 + 0] = data[i * 3 + 0];
rgbadata[i * 4 + 1] = data[i * 3 + 1];
rgbadata[i * 4 + 2] = data[i * 3 + 2];
rgbadata[i * 4 + 3] =
data[i * 3 + 0] + data[i * 3 + 1] + data[i * 3 + 2] ? 255 : 0;
}
}else{
//单波段则数据为一个一组,rgbadata为rgba四个一组(这里颜色有点问题,应根据波段信息计算颜色)
for (let i = 0; i < data.length; i++) {
rgbadata[i * 4 + 0] = data[i];
rgbadata[i * 4 + 1] = data[i];
rgbadata[i * 4 + 2] = data[i];
rgbadata[i * 4 + 3] = data[i] ? 255 : 0;
}
}
const img = new ImageData(
new Uint8ClampedArray(rgbadata.buffer),
this.tileSize,
this.tileSize
);
const canvas = document.createElement("canvas");
canvas.width = canvas.height = this.tileSize;
var ctx = canvas.getContext("2d");
ctx.putImageData(img, 0, 0);
return canvas;
}
}
export { CogSource }