feat: 栅格瓦片的多波段计算 (#1367)

* fix: 修复 featureScale 错误

* style: lint style

* fix: remove featureScalePlugin async

* feat: 增加多波段瓦片的 operation 操作

* style: lint style

* feat: 增加波段指定逻辑

* style: lint style

* feat: 优化多波段的请求操作路径代码

* style: lint style

* feat: multi raster layer support express operation 1.0

* style: lint style

* feat: 增加栅格图层的多波段分析能力

* chore: add raster layer demo

* style: lint style

* style: lint style

* style: lint style

* style: lint style

* style: lint style

* feat: 补充栅格数据表达式计算

* chore: 调整栅格计算方法的位置

* chore: 优化栅格计算代码结构

* chore: 优化数据栅格代码

* style: lint style

* style: lint style

* style: lint style

* feat: band operation handle empty data

* feat: 支持彩色多通道栅格

* feat: 数据栅格瓦片支持彩色多通道波段计算

* style: lint style

* style: lint style

* style: lint style

Co-authored-by: shihui <yiqianyao.yqy@alibaba-inc.com>
This commit is contained in:
YiQianYao 2022-10-10 19:15:24 +08:00 committed by GitHub
parent 36dcd9928c
commit 5540d4c610
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1585 additions and 502 deletions

View File

@ -24,57 +24,50 @@ async function getTiffData() {
};
}
async function addLayer() {
const tiffdata = await getTiffData();
const layer = new RasterLayer({});
layer
.source(tiffdata.data, {
parser: {
type: 'raster',
width: tiffdata.width,
height: tiffdata.height,
min: 0,
max: 80,
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
heightRatio: 100,
opacity: 0.8,
domain: [0, 2000],
rampColors: {
colors: [
'#FF4818',
'#F7B74A',
'#FFF598',
'#91EABC',
'#2EA9A1',
'#206C7C',
].reverse(),
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
},
});
return layer;
}
export default () => {
useEffect(() => {
(async () => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [121.268, 30.3628],
zoom: 3,
}),
});
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [121.268, 30.3628],
zoom: 3,
}),
});
const layer = await addLayer();
scene.on('loaded', async () => {
const tiffdata = await getTiffData();
scene.on('loaded', () => {
scene.addLayer(layer);
});
})();
const layer = new RasterLayer({});
layer
.source(tiffdata.data, {
parser: {
type: 'raster',
width: tiffdata.width,
height: tiffdata.height,
min: 0,
max: 80,
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
heightRatio: 100,
opacity: 0.8,
domain: [0, 2000],
rampColors: {
colors: [
'#FF4818',
'#F7B74A',
'#FFF598',
'#91EABC',
'#2EA9A1',
'#206C7C',
].reverse(),
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
},
});
scene.addLayer(layer);
});
}, []);
return (
<div

View File

@ -0,0 +1,84 @@
// @ts-ignore
import { RasterLayer, Scene } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
import * as GeoTIFF from 'geotiff';
async function getTiffData() {
const response = await fetch(
'https://gw.alipayobjects.com/os/rmsportal/XKgkjjGaAzRyKupCBiYW.dat',
);
const arrayBuffer = await response.arrayBuffer();
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer);
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const values = await image.readRasters();
return {
data: values[0],
width,
height,
min: 0,
max: 8000,
};
}
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [121.268, 30.3628],
zoom: 3,
}),
});
scene.on('loaded', async () => {
const tiffdata = await getTiffData();
const layer = new RasterLayer({});
layer
.source(tiffdata.data, {
parser: {
type: 'raster',
width: tiffdata.width,
height: tiffdata.height,
min: 0,
max: 80,
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
heightRatio: 100,
opacity: 0.8,
domain: [0, 2000],
rampColors: {
colors: [
'#FF4818',
'#F7B74A',
'#FFF598',
'#91EABC',
'#2EA9A1',
'#206C7C',
].reverse(),
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
},
});
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,93 @@
// @ts-ignore
import { RasterLayer, Scene } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
import * as GeoTIFF from 'geotiff';
async function getTiffData() {
const response = await fetch(
'https://gw.alipayobjects.com/os/rmsportal/XKgkjjGaAzRyKupCBiYW.dat',
// 'http://127.0.0.1:3333/p.tif',
);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer
}
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [121.268, 30.3628],
zoom: 3,
}),
});
scene.on('loaded', async () => {
const tiffdata = await getTiffData();
const layer = new RasterLayer({})
layer.source({
data: tiffdata,
bands: [0]
}, {
parser: {
type: 'raster',
// width: tiffdata.width,
// height: tiffdata.height,
format: async (data, bands) => {
const tiff = await GeoTIFF.fromArrayBuffer(data);
const imageCount = await tiff.getImageCount();
console.log('imageCount', imageCount)
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const values = await image.readRasters();
return { rasterData: values[0], width, height };
},
// operation: (allBands) => {
// return allBands[0].rasterData;
// },
operation: ['+', ['band', 0], 1],
min: 0,
max: 80,
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
heightRatio: 100,
opacity: 0.8,
domain: [0, 2000],
rampColors: {
colors: [
'#FF4818',
'#F7B74A',
'#FFF598',
'#91EABC',
'#2EA9A1',
'#206C7C',
].reverse(),
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
},
});
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,94 @@
// @ts-ignore
import { RasterLayer, Scene } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
import * as GeoTIFF from 'geotiff';
async function getTiffData(url: string) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer
}
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [110, 30.3628],
zoom: 3,
}),
});
scene.on('loaded', async () => {
//
const url1 = 'https://gw.alipayobjects.com/os/rmsportal/XKgkjjGaAzRyKupCBiYW.dat';
// 全国夜光图
const url2 = 'https://gw.alipayobjects.com/zos/antvdemo/assets/light_clip/lightF182013.tiff'
const tiffdata = await getTiffData(url1);
const tiffdata2 = await getTiffData(url2);
// const rasterData = { data: tiffdata }
const rasterData = [
{ data: tiffdata },
{ data: tiffdata2 }
];
const layer = new RasterLayer({})
layer.source(rasterData, {
parser: {
type: 'raster',
format: async (data, bands) => {
// console.log('bands', bands)
const tiff = await GeoTIFF.fromArrayBuffer(data);
// const imageCount = await tiff.getImageCount();
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const values = await image.readRasters();
return { rasterData: values[0], width, height };
},
// operation: (allBands) => {
// console.log(allBands)
// return allBands[0].rasterData;
// },
operation: ['+', ['+', ['band', 0], 90], ['*', ['band', 1], 50]],
min: 0,
max: 80,
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
heightRatio: 100,
opacity: 0.8,
domain: [0, 4000],
rampColors: {
colors: [
'#FF4818',
'#F7B74A',
'#FFF598',
'#333',
'#222',
'#000',
].reverse(),
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
},
});
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,85 @@
// @ts-ignore
import { RasterLayer, Scene } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
import * as GeoTIFF from 'geotiff';
async function getTiffData(url: string) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer
}
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [121.268, 30.3628],
zoom: 3,
}),
});
scene.on('loaded', async () => {
const url1 = 'https://gw.alipayobjects.com/os/rmsportal/XKgkjjGaAzRyKupCBiYW.dat';
const url2 = 'https://gw.alipayobjects.com/zos/antvdemo/assets/light_clip/lightF182013.tiff'
const tiffdata = await getTiffData(url1);
const tiffdata2 = await getTiffData(url2);
const layer = new RasterLayer({})
layer.source([
{
data: tiffdata,
bands: [0],
},
{
data: tiffdata2,
bands: [0],
}
], {
parser: {
type: 'rasterRgb',
format: async (data, bands) => {
// console.log(bands, )
const tiff = await GeoTIFF.fromArrayBuffer(data);
const imageCount = await tiff.getImageCount();
console.log('imageCount', imageCount, bands)
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const values = await image.readRasters();
return { rasterData: values[0], width, height };
},
// operation: (allBands) => {
// return allBands[0].rasterData;
// },
// operation: ['+', ['band', 0], 1],
operation: {
r: ['*', ['band', 1], 1],
g: ['*', ['band', 1], 1],
b: ['*', ['band', 1], 1],
},
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
// opacity: 0.8,
channelRMax: 100,
channelGMax: 100,
channelBMax: 100
});
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,118 @@
// @ts-ignore
import { RasterLayer, Scene } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
async function getTiffData(url: string) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer
}
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [110, 30.3628],
zoom: 3,
}),
});
const canvas = document.createElement('canvas');
const canvasSize = 256;
canvas.width = canvasSize;
canvas.height = canvasSize;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
scene.on('loaded', async () => {
const url1 = 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*sV6gSYSdpl4AAAAAAAAAAAAAARQnAQ';
const tiffdata = await getTiffData(url1);
// Gray = R*0.299 + G*0.587 + B*0.114
const grayExp = ['+',
['*', ['band', 0], 0.299],
[
'+',
['*', ['band', 1], 0.587],
['*', ['band', 2], 0.114]
]
]
const layer = new RasterLayer({})
layer.source([
{
data: tiffdata,
bands: [0],
},
], {
parser: {
type: 'rasterRgb',
format: async (data, bands) => {
const blob: Blob = new Blob([new Uint8Array(data)], {
type: 'image/png',
});
const img = await createImageBitmap(blob);
ctx.clearRect(0, 0, canvasSize, canvasSize);
ctx.drawImage(img, 0, 0, canvasSize, canvasSize);
const imgData = ctx.getImageData(0, 0, canvasSize, canvasSize).data;
const channelR: number[] = [];
const channelG: number[] = [];
const channelB: number[] = [];
for (let i = 0; i < imgData.length; i += 4) {
const R = imgData[i];
const G = imgData[i + 1];
const B = imgData[i + 2];
channelR.push(R);
channelG.push(G);
channelB.push(B);
}
return [
{ rasterData: channelR, width: canvasSize, height: canvasSize },
{ rasterData: channelG, width: canvasSize, height: canvasSize },
{ rasterData: channelB, width: canvasSize, height: canvasSize }
];
},
// operation: (allBands) => {
// return allBands[0].rasterData;
// },
// operation: ['+', ['band', 0], 1],
operation: {
r: ['band', 0],
g: ['band', 1],
b: ['band', 2],
// r: ['*', ['band', 0], 1],
// g: ['*', ['band', 1], 1],
// b: ['*', ['band', 2], 1],
// r: grayExp,
// g: grayExp,
// b: grayExp,
},
extent: [73.482190241, 3.82501784112, 135.106618732, 57.6300459963],
},
})
.style({
// opacity: 0.8,
// channelRMax: 100,
// channelGMax: 100,
// channelBMax: 100
});
scene.addLayer(layer);
});
return () => {
scene.destroy();
}
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,3 @@
### RasterLayer - raster
#### 普通 rasterLayer
<code src="./demos/rasterData.tsx"></code>

View File

@ -0,0 +1,4 @@
### RasterLayer - rasterFile
#### raster file data
加载单个栅格文件的二进制流数据,自己提供栅格波段数据的提取方法。
<code src="./demos/rasterFile.tsx"></code>

View File

@ -0,0 +1,4 @@
### RasterLayer - rasterFile2
#### raster file data2
加载多个栅格文件的二进制流数据,自己提供栅格波段数据的提取方法。
<code src="./demos/rasterFile2.tsx"></code>

View File

@ -0,0 +1,3 @@
### RasterLayer - rasterRgb
#### 普通 rgb rasterLayer
<code src="./demos/rasterRgb.tsx"></code>

View File

@ -0,0 +1,3 @@
### RasterLayer - rasterRgb2
#### 普通 rgb2 rasterLayer
<code src="./demos/rasterRgb2.tsx"></code>

View File

@ -28,7 +28,8 @@ export default () => {
'https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/{z}/{x}/{y}.vector.pbf?access_token=';
const token =
// 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2s5OXVzdHlzMDVneDNscDVjdzVmeXl0dyJ9.81SQ5qaJS0xExYLbDZAGpQ';
'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw';
// 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw';
'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w4bXNyeHgzMGl0cjNvbXlmeHFjeDBwZCJ9.05W7JfyT6BVkpu12dYL58w';
const source = new Source(url + token, {
parser: {
type: 'mvt',

View File

@ -27,7 +27,8 @@ export default () => {
'https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/{z}/{x}/{y}.vector.pbf?access_token=';
const token =
// 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2s5OXVzdHlzMDVneDNscDVjdzVmeXl0dyJ9.81SQ5qaJS0xExYLbDZAGpQ';
'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw';
// 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw';
'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w4bXNyeHgzMGl0cjNvbXlmeHFjeDBwZCJ9.05W7JfyT6BVkpu12dYL58w';
const source = new Source(url + token, {
parser: {
type: 'mvt',

View File

@ -1,3 +1,3 @@
### Raster - HillShade
山体阴影
<code src="./rasterData/hillshade.tsx"></code>
<code src="./rasterData/hillShading.tsx"></code>

View File

@ -12,7 +12,7 @@ export default () => {
center: [-119.5591, 37.715],
zoom: 9,
style: 'mapbox://styles/mapbox/cjaudgl840gn32rnrepcb9b9g',
token: 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw'
token: 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w4bXNyeHgzMGl0cjNvbXlmeHFjeDBwZCJ9.05W7JfyT6BVkpu12dYL58w'
}),
});
@ -26,7 +26,7 @@ export default () => {
layer
.source(
// 'https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw',
'https://api.mapbox.com/raster/v1/mapbox.mapbox-terrain-dem-v1/{z}/{x}/{y}.webp?sku=101xrmKJip7uQ&access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw',
'https://api.mapbox.com/raster/v1/mapbox.mapbox-terrain-dem-v1/{z}/{x}/{y}.webp?sku=101xrmKJip7uQ&access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w4bXNyeHgzMGl0cjNvbXlmeHFjeDBwZCJ9.05W7JfyT6BVkpu12dYL58w',
// 'https://b.tiles.mapbox.com/v3/aj.sf-dem/12/657/1589.png',
// https://b.tiles.mapbox.com/v3/aj.sf-dem/12/659/1589.png
{

View File

@ -1,205 +0,0 @@
//@ts-ignore
import { Scene, RasterLayer } from '@antv/l7';
//@ts-ignore
import { Mapbox } from '@antv/l7-maps';
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new Mapbox({
center: [-119.5591, 37.715],
zoom: 9,
style: 'mapbox://styles/mapbox/cjaudgl840gn32rnrepcb9b9g',
token: 'pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw'
}),
});
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
scene.on('loaded', () => {
const layer = new RasterLayer();
layer
.source(
// 'https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw',
'https://api.mapbox.com/raster/v1/mapbox.mapbox-terrain-dem-v1/{z}/{x}/{y}.webp?sku=101xrmKJip7uQ&access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw',
// 'https://b.tiles.mapbox.com/v3/aj.sf-dem/12/657/1589.png',
// https://b.tiles.mapbox.com/v3/aj.sf-dem/12/659/1589.png
{
parser: {
type: 'rasterTile',
dataType: 'arraybuffer',
tileSize: 256,
// extent: [-180, -85.051129, 179, 85.051129],
format: async (data: any) => {
const blob: Blob = new Blob([new Uint8Array(data)], {
type: 'image/png',
});
const img = await createImageBitmap(blob);
ctx.clearRect(0, 0, 256, 256);
ctx.drawImage(img, 0, 0, 256, 256);
const shadeOptions = {
resolution: 256,
sunEl: 0,
sunAz: 131,
vert: 2
}
const elevationImage = ctx.getImageData(0, 0, 256, 256);
const width = elevationImage.width;
const height = elevationImage.height;
const elevationData = elevationImage.data;
const shadeData = new Uint8ClampedArray(elevationData.length);
const dp = shadeOptions.resolution * 2;
const maxX = width - 1;
const maxY = height - 1;
const pixel = [0, 0, 0, 0];
const twoPi = 2 * Math.PI;
const halfPi = Math.PI / 2;
const sunEl = (Math.PI * shadeOptions.sunEl) / 180;
const sunAz = (Math.PI * shadeOptions.sunAz) / 180;
const cosSunEl = Math.cos(sunEl);
const sinSunEl = Math.sin(sunEl);
let pixelX,
pixelY,
x0,
x1,
y0,
y1,
offset,
z0,
z1,
dzdx,
dzdy,
slope,
aspect,
cosIncidence,
scaled;
function calculateElevation(pixel) {
// The method used to extract elevations from the DEM.
// In this case the format used is
// red + green * 2 + blue * 3
//
// Other frequently used methods include the Mapbox format
// (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
// and the Terrarium format
// (red * 256 + green + blue / 256) - 32768
//
// return pixel[0] + pixel[1] * 2 + pixel[2] * 3;
return -10000 + (pixel[0] * 256 * 256 + pixel[1] * 2 * 256 + pixel[2]) * 0.1;
}
const arr2: number[] = [];
for (pixelY = 0; pixelY <= maxY; ++pixelY) {
y0 = pixelY === 0 ? 0 : pixelY - 1;
y1 = pixelY === maxY ? maxY : pixelY + 1;
for (pixelX = 0; pixelX <= maxX; ++pixelX) {
x0 = pixelX === 0 ? 0 : pixelX - 1;
x1 = pixelX === maxX ? maxX : pixelX + 1;
// determine elevation for (x0, pixelY)
offset = (pixelY * width + x0) * 4;
pixel[0] = elevationData[offset];
pixel[1] = elevationData[offset + 1];
pixel[2] = elevationData[offset + 2];
pixel[3] = elevationData[offset + 3];
z0 = shadeOptions.vert * calculateElevation(pixel);
// determine elevation for (x1, pixelY)
offset = (pixelY * width + x1) * 4;
pixel[0] = elevationData[offset];
pixel[1] = elevationData[offset + 1];
pixel[2] = elevationData[offset + 2];
pixel[3] = elevationData[offset + 3];
z1 = shadeOptions.vert * calculateElevation(pixel);
dzdx = (z1 - z0) / dp;
// determine elevation for (pixelX, y0)
offset = (y0 * width + pixelX) * 4;
pixel[0] = elevationData[offset];
pixel[1] = elevationData[offset + 1];
pixel[2] = elevationData[offset + 2];
pixel[3] = elevationData[offset + 3];
z0 = shadeOptions.vert * calculateElevation(pixel);
// determine elevation for (pixelX, y1)
offset = (y1 * width + pixelX) * 4;
pixel[0] = elevationData[offset];
pixel[1] = elevationData[offset + 1];
pixel[2] = elevationData[offset + 2];
pixel[3] = elevationData[offset + 3];
z1 = shadeOptions.vert * calculateElevation(pixel);
dzdy = (z1 - z0) / dp;
slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy));
aspect = Math.atan2(dzdy, -dzdx);
if (aspect < 0) {
aspect = halfPi - aspect;
} else if (aspect > halfPi) {
aspect = twoPi - aspect + halfPi;
} else {
aspect = halfPi - aspect;
}
cosIncidence =
sinSunEl * Math.cos(slope) +
cosSunEl * Math.sin(slope) * Math.cos(sunAz - aspect);
offset = (pixelY * width + pixelX) * 4;
scaled = 255 * cosIncidence;
shadeData[offset + 1] = scaled;
shadeData[offset + 2] = scaled;
shadeData[offset + 3] = elevationData[offset + 3];
const r = shadeData[offset]
const g = shadeData[offset + 1]
const b = shadeData[offset + 2]
// (0.30*R)+(0.59*G)+(0.11*B)
arr2.push( 0.30 * r + 0.59 * g + 0.11 * b );
}
}
return {rasterData: arr2, width: width, height: height};
},
},
},
)
.style({
opacity: 0.5,
domain: [0, 255],
rampColors: {
colors: [
'#fff',
'#000',
],
positions: [0, 1.0],
},
});
scene.addLayer(layer)
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -25,8 +25,7 @@ export default () => {
const layer = new RasterLayer();
layer
.source(
'https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=pk.eyJ1IjoiMTg5Njk5NDg2MTkiLCJhIjoiY2w3dHk3dnN4MDYzaDNycDkyMDl2bzh6NiJ9.YIrG9kwUpayLj01f6W23Gw',
'http://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
{
parser: {
type: 'rasterTile',
@ -46,10 +45,7 @@ export default () => {
const arr: number[] = [];
for (let i = 0; i < imgData.length; i += 4) {
const R = imgData[i];
const G = imgData[i + 1];
const B = imgData[i + 2];
const d = -10000 + (R * 256 * 256 + G * 256 + B) * 0.1;
arr.push(d);
arr.push(R);
}
return {
rasterData: arr,
@ -61,7 +57,7 @@ export default () => {
},
)
.style({
domain: [0, 1014],
domain: [0, 255],
clampLow: true,
rampColors: {
colors: [

View File

@ -0,0 +1,94 @@
//@ts-ignore
import { Scene, RasterLayer } from '@antv/l7';
//@ts-ignore
import { Map } from '@antv/l7-maps';
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map2',
map: new Map({
center: [105.732421875, 32.24997445586331],
pitch: 0,
style: 'dark',
zoom: 2,
}),
});
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
scene.on('loaded', () => {
const layer = new RasterLayer();
layer
.source(
'http://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
{
parser: {
type: 'rasterTile',
// dataType: 'arraybuffer',
dataType: 'rgb',
tileSize: 256,
zoomOffset: 0,
extent: [-180, -85.051129, 179, 85.051129],
minZoom: 0,
format: async (data: any, bands) => {
// console.log(bands)
const blob: Blob = new Blob([new Uint8Array(data)], {
type: 'image/png',
});
const img = await createImageBitmap(blob);
ctx.clearRect(0, 0, 256, 256);
ctx.drawImage(img, 0, 0, 256, 256);
const imgData = ctx.getImageData(0, 0, 256, 256).data;
const channelR: number[] = [];
const channelG: number[] = [];
const channelB: number[] = [];
for (let i = 0; i < imgData.length; i += 4) {
const R = imgData[i];
const G = imgData[i + 1];
const B = imgData[i + 2];
channelR.push(R);
channelG.push(G);
channelB.push(B);
}
return [
{ rasterData: channelR, width: 256, height: 256 },
{ rasterData: channelG, width: 256, height: 256 },
{ rasterData: channelB, width: 256, height: 256 }
]
},
// operation: ['band', 0]
// operation: (allBands) => {
// // console.log(allBands)
// return allBands[0].rasterData
// }
operation: {
r: ['band', 0],
g: ['band', 1],
b: ['band', 2],
}
},
},
)
.style({
});
scene.addLayer(layer)
});
}, []);
return (
<div
id="map2"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -1,4 +1,6 @@
// @ts-ignore
import { RasterLayer, Scene, Source } from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
import * as GeoTIFF from 'geotiff';
@ -80,9 +82,19 @@ export default () => {
maskfence: maskData
});
// const urls = [
// 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff',
// 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff',
// ]
const urls = [
'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff',
'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff',
{
url: 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff',
bands: [0]
},
{
url: 'https://ganos.oss-cn-hangzhou.aliyuncs.com/m2/l7/tiff_jx/{z}/{x}/{y}.tiff'
// default bands: [0]
}
]
const tileSource = new Source(urls,
{
@ -91,29 +103,38 @@ export default () => {
dataType: 'arraybuffer',
tileSize: 256,
maxZoom: 13.1,
format: async data => {
format: async (data, bands) => {
// current raster file bands
console.log('bands', bands)
const tiff = await GeoTIFF.fromArrayBuffer(data[0]);
const image = await tiff.getImage();
const tiff = await GeoTIFF.fromArrayBuffer(data);
const image = await tiff.getImage(bands[0]);
const width = image.getWidth();
const height = image.getHeight();
const values = await image.readRasters();
const rasterData = values[0];
// return bandData | bandData[]
// return [{ rasterData, width, height }];
return { rasterData, width, height };
},
// bands: [0, 1]
operation: ['*', ['band', 0], 2],
// operation: (allBands) => {
// const rasterData: number[] = [];
// const { width, height } = allBands[0];
// const length = width * height;
// const band0 = allBands[0];
// const band1 = allBands[1];
const tiff2 = await GeoTIFF.fromArrayBuffer(data[1]);
const image2 = await tiff2.getImage();
const values2 = await image2.readRasters();
const rasterData2 = values2[0];
const r = rasterData.map((d, i) => {
const d2 = rasterData2[i]
return d/2 + d2/3
})
return { rasterData: r, width, height };
}
// for(let i = 0;i < length; i++) {
// const v1 = band0.rasterData[i] | 0;
// const v2 = band1.rasterData[i] | 0;
// rasterData.push(v1 * (1/2) + v2 * (1/2))
// }
// return rasterData;
// // return bands[0].rasterData;
// }
}
});

View File

@ -0,0 +1,5 @@
### Raster - RasterData RGB
彩色数据栅格(多通道)
#### 加载 image
<code src="./rasterData/loadImageRGB.tsx"></code>

View File

@ -579,7 +579,7 @@ export default class BaseLayer<ChildLayerStyleOptions = {}>
}
public source(data: any, options?: ISourceCFG): ILayer {
if (data?.data) {
if (data?.type === 'source') {
// 判断是否为source
this.setSource(data);
return this;

View File

@ -279,4 +279,8 @@ export interface IRasterLayerStyleOptions extends IBaseLayerStyleOptions {
clampHigh: boolean;
rampColors: IColorRamp;
rampColorsData?: ImageData | IImagedata;
channelRMax?: number;
channelGMax?: number;
channelBMax?: number;
}

View File

@ -30,6 +30,7 @@ export default class RaterLayer extends BaseLayer<IRasterLayerStyleOptions> {
const type = this.getModelType();
const defaultConfig = {
raster: {},
rasterRgb: {},
raster3d: {},
rasterTile: {},
};
@ -42,6 +43,8 @@ export default class RaterLayer extends BaseLayer<IRasterLayerStyleOptions> {
switch (parserType) {
case 'raster':
return 'raster';
case 'rasterRgb':
return 'rasterRgb';
case 'rasterTile':
return 'rasterTile';
default:

View File

@ -1,9 +1,11 @@
import RasterTileModel from '../../tile/models/tileModel';
import RasterModel from './raster';
export type RasterModelType = 'raster' | 'raster3d' | 'rasterTile';
import RasterRgbModel from './rasterRgb';
export type RasterModelType = 'raster' | 'raster3d' | 'rasterTile' | 'rasterRgb';
const RasterModels: { [key in RasterModelType]: any } = {
raster: RasterModel,
rasterRgb: RasterRgbModel,
raster3d: RasterModel,
rasterTile: RasterTileModel,
};

View File

@ -41,7 +41,26 @@ export default class RasterModel extends BaseModel {
};
}
public initModels(callbackModel: (models: IModel[]) => void) {
private async getRasterData(parserDataItem: any) {
if(Array.isArray(parserDataItem.data)) {
// 直接传入波段数据
return {
data: parserDataItem.data,
width: parserDataItem.width,
height: parserDataItem.height,
}
} else {
// 多波段形式、需要进行处理
const { rasterData, width, height } = await parserDataItem.data;
return {
data: Array.from(rasterData),
width,
height
}
}
}
public async initModels(callbackModel: (models: IModel[]) => void) {
const {
mask = false,
maskInside = true,
@ -51,10 +70,13 @@ export default class RasterModel extends BaseModel {
const source = this.layer.getSource();
const { createTexture2D } = this.rendererService;
const parserDataItem = source.data.dataArray[0];
const {data, width, height} = await this.getRasterData(parserDataItem);
this.texture = createTexture2D({
data: parserDataItem.data,
width: parserDataItem.width,
height: parserDataItem.height,
data,
width,
height,
format: gl.LUMINANCE,
type: gl.FLOAT,
// aniso: 4,

View File

@ -0,0 +1,130 @@
import {
AttributeType,
gl,
IEncodeFeature,
IModel,
ITexture2D,
} from '@antv/l7-core';
import { getMask } from '@antv/l7-utils';
import BaseModel from '../../core/BaseModel';
import { IRasterLayerStyleOptions } from '../../core/interface';
import { RasterImageTriangulation } from '../../core/triangulation';
import rasterFrag from '../shaders/raster_rgb_frag.glsl';
import rasterVert from '../shaders/raster_2d_vert.glsl';
export default class RasterModel extends BaseModel {
protected texture: ITexture2D;
protected channelRMax: number = 256;
protected channelGMax: number = 256;
protected channelBMax: number = 256;
public getUninforms() {
const {
opacity = 1,
channelRMax,
channelGMax,
channelBMax
} = this.layer.getLayerConfig() as IRasterLayerStyleOptions;
return {
u_opacity: opacity || 1,
u_texture: this.texture,
u_channelRMax: channelRMax !== undefined ? channelRMax : this.channelRMax,
u_channelGMax: channelGMax !== undefined ? channelGMax : this.channelGMax,
u_channelBMax: channelBMax !== undefined ? channelBMax : this.channelBMax,
};
}
private async getRasterData(parserDataItem: any) {
if(Array.isArray(parserDataItem.data)) {
// 直接传入波段数据
return {
data: parserDataItem.data,
width: parserDataItem.width,
height: parserDataItem.height,
}
} else {
// 多波段形式、需要进行处理
// 支持彩色栅格(多通道)
const { rasterData, width, height, channelR, channelG, channelB } = await parserDataItem.data;
this.channelRMax = channelR;
this.channelGMax = channelG;
this.channelBMax = channelB;
return {
data: Array.from(rasterData),
width,
height
}
}
}
public async initModels(callbackModel: (models: IModel[]) => void) {
const {
mask = false,
maskInside = true,
} = this.layer.getLayerConfig() as IRasterLayerStyleOptions;
const source = this.layer.getSource();
const { createTexture2D } = this.rendererService;
const parserDataItem = source.data.dataArray[0];
const {data, width, height} = await this.getRasterData(parserDataItem);
this.texture = createTexture2D({
// @ts-ignore
data,
width,
height,
format: gl.RGB,
type: gl.FLOAT,
});
this.layer
.buildLayerModel({
moduleName: 'rasterImageDataRGBA',
vertexShader: rasterVert,
fragmentShader: rasterFrag,
triangulation: RasterImageTriangulation,
primitive: gl.TRIANGLES,
depth: { enable: false },
stencil: getMask(mask, maskInside),
pick: false,
})
.then((model) => {
callbackModel([model]);
})
.catch((err) => {
console.warn(err);
callbackModel([]);
});
}
public buildModels(callbackModel: (models: IModel[]) => void) {
this.initModels(callbackModel);
}
public clearModels(): void {
this.texture?.destroy();
}
protected registerBuiltinAttributes() {
// point layer size;
this.styleAttributeService.registerStyleAttribute({
name: 'uv',
type: AttributeType.Attribute,
descriptor: {
name: 'a_Uv',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.DYNAMIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 2,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
) => {
return [vertex[3], vertex[4]];
},
},
});
}
}

View File

@ -0,0 +1,12 @@
precision mediump float;
uniform float u_opacity: 1.0;
uniform sampler2D u_texture;
uniform float u_channelRMax: 256.;
uniform float u_channelGMax: 256.;
uniform float u_channelBMax: 256.;
varying vec2 v_texCoord;
void main() {
vec3 rgb = texture2D(u_texture,vec2(v_texCoord.x,v_texCoord.y)).rgb;
gl_FragColor = vec4(rgb.r/u_channelRMax, rgb.g/u_channelGMax, rgb.b/u_channelBMax, u_opacity);
}

View File

@ -22,7 +22,6 @@ export default class RasterTiffTile extends TileFactory {
clampLow,
mask,
} = initOptions;
const rasterData = tile.data;
if (!rasterData.data) {
console.warn('raster data not exist!');
@ -31,13 +30,15 @@ export default class RasterTiffTile extends TileFactory {
layerIDList: [],
};
}
const dataType = this.parentLayer?.getSource()?.parser?.dataType;
const layer = new RasterDataLayer({
visible: tile.isVisible,
mask,
})
.source(rasterData.data, {
parser: {
type: 'raster',
// 数据栅格分为单通道栅格和多通道彩色栅格
type: dataType === 'rgb' ? 'rasterRgb': 'raster',
width: rasterData.width,
height: rasterData.height,
extent: tile.bboxPolygon.bbox,
@ -46,6 +47,7 @@ export default class RasterTiffTile extends TileFactory {
.style({
colorTexture,
opacity,
// TODO: 目前从 domain 从父瓦片图层的 style 进行配置,后续考虑从每个时机请求的栅格文件中进行配置
domain,
clampHigh,
clampLow,

View File

@ -1,6 +1,7 @@
import BaseLayer from '../../core/BaseLayer';
import { IRasterLayerStyleOptions } from '../../core/interface';
import RasterModel from '../../raster/models/rasterTile';
import RasterRgbModel from '../../raster/models/rasterRgb';
export default class RasterTiffLayer extends BaseLayer<
Partial<IRasterLayerStyleOptions>
@ -21,7 +22,11 @@ export default class RasterTiffLayer extends BaseLayer<
}
protected getModelType() {
return RasterModel;
if(this.layerSource.parser.type === 'rasterRgb') {
return RasterRgbModel;
} else {
return RasterModel;
}
}
protected getConfigSchema() {
return {

View File

@ -6,6 +6,7 @@ import json from './parser/json';
import mapboxVectorTile from './parser/mvt';
import geojsonVTTile from './parser/geojsonvt';
import raster from './parser/raster';
import rasterRgb from './parser/rasterRgb';
import rasterTile, { rasterDataTypes } from './parser/raster-tile';
import testTile from './parser/testTile';
import Source from './source';
@ -25,6 +26,7 @@ registerParser('image', image);
registerParser('csv', csv);
registerParser('json', json);
registerParser('raster', raster);
registerParser('rasterRgb', rasterRgb);
registerTransform('cluster', cluster);
registerTransform('filter', filter);
registerTransform('join', join);

View File

@ -26,6 +26,7 @@ export interface IParserData {
export enum RasterTileType {
IMAGE = 'image',
ARRAYBUFFER = 'arraybuffer',
RGB = 'rgb',
}
export interface IGeojsonvtOptions {
@ -58,9 +59,41 @@ export interface ITileParserCFG {
geojsonvtOptions?: IGeojsonvtOptions;
format?: any;
operation?: any;
}
export interface IJsonItem {
[key: string]: any;
}
export type IJsonData = IJsonItem[];
export interface IRasterData {
rasterData: HTMLImageElement | Uint8Array| ImageBitmap | null | undefined;
width: number;
height: number;
}
export type IRasterFormat = (imageData: ArrayBuffer, bands: number[], channels?: string[]) => Promise<IRasterData|IRasterData[]>;
export interface IRasterFileData {
data: ArrayBuffer;
bands: number[];
}
export type IRgbOperation = {
r?: any[];
g?: any[]
b?: any[]
};
export type IBandsOperation = ((bands: IRasterData[]) => Uint8Array | Array<number>) | any[] | IRgbOperation;
export type IRasterLayerData = number[] | IRasterFileData | IRasterFileData[];
export interface IRasterCfg {
format?: IRasterFormat;
operation?: IBandsOperation;
extent: [number, number, number, number];
width: number;
height: number;
max: number;
min: number;
}

View File

@ -1,6 +1,15 @@
import { Tile, TileLoadParams, TilesetManagerOptions } from '@antv/l7-utils';
import {
Tile,
TileLoadParams,
TilesetManagerOptions,
ITileBand,
} from '@antv/l7-utils';
import { IParserData, ITileParserCFG, RasterTileType } from '../interface';
import { defaultFormat, getTileBuffer, getTileImage } from '../utils/getTile';
import {
defaultFormat,
getTileBuffer,
getTileImage,
} from '../utils/tile/getRasterTile';
const DEFAULT_CONFIG: Partial<TilesetManagerOptions> = {
tileSize: 256,
@ -9,7 +18,13 @@ const DEFAULT_CONFIG: Partial<TilesetManagerOptions> = {
zoomOffset: 0,
};
export const rasterDataTypes = [RasterTileType.ARRAYBUFFER];
export const rasterDataTypes = [RasterTileType.ARRAYBUFFER, RasterTileType.RGB];
function isUrlError(url: string | string[] | ITileBand[]) {
if (Array.isArray(url) && url.length === 0) return true;
if (!Array.isArray(url) && typeof url !== 'string') return true;
return false;
}
/**
*
* @param data
@ -17,23 +32,29 @@ export const rasterDataTypes = [RasterTileType.ARRAYBUFFER];
* @returns
*/
export default function rasterTile(
data: string | string[],
data: string | string[] | ITileBand[],
cfg?: ITileParserCFG,
): IParserData {
const tileDataType: RasterTileType = cfg?.dataType || RasterTileType.IMAGE;
if (isUrlError(data)) throw new Error('tile server url is error');
let tileDataType: RasterTileType = cfg?.dataType || RasterTileType.IMAGE;
// Tip: RasterTileType.RGB 是彩色多通道的数据纹理,同样走数据纹理的请求
if (tileDataType === RasterTileType.RGB)
tileDataType = RasterTileType.ARRAYBUFFER;
const getTileData = (tileParams: TileLoadParams, tile: Tile) => {
switch (tileDataType) {
case RasterTileType.IMAGE:
return getTileImage(data, tileParams, tile);
return getTileImage(data as string | string[], tileParams, tile);
case RasterTileType.ARRAYBUFFER:
return getTileBuffer(
data,
tileParams,
tile,
cfg?.format || defaultFormat,
cfg?.operation,
);
default:
return getTileImage(data, tileParams, tile);
return getTileImage(data as string | string[], tileParams, tile);
}
};

View File

@ -1,14 +1,36 @@
import { IParserData, IRasterCfg } from '@antv/l7-core';
export default function raster(data: number[], cfg: IRasterCfg): IParserData {
const { extent, width, height, min, max } = cfg;
import { IParserData } from '@antv/l7-core';
import { IRasterLayerData, IRasterCfg, IRasterFileData } from '../interface';
import { bandsOperation } from '../utils/bandOperation/bands';
import { isNumberArray } from '../utils/util';
export default function raster(
data: IRasterLayerData,
cfg: IRasterCfg,
): IParserData {
const { extent, width, height, min, max, format, operation } = cfg;
let bandData, rasterWidth, rasterHeight;
if (format === undefined || isNumberArray(data)) {
// 兼容写法 - 用户直接传入解析完的波段数据
bandData = Array.from(data as number[]);
rasterWidth = width;
rasterHeight = height;
} else {
// 用户传入为解析的栅格数据 - arraybuffer
// 将数据统一为 IRasterFileData[]
const imageDataList = (Array.isArray(data)
? data
: [data]) as IRasterFileData[];
bandData = bandsOperation(imageDataList, format, operation);
}
const resultData = {
_id: 1,
dataArray: [
{
_id: 1,
data: Array.from(data),
width,
height,
data: bandData,
width: rasterWidth,
height: rasterHeight,
min,
max,
coordinates: [

View File

@ -0,0 +1,44 @@
import { IParserData } from '@antv/l7-core';
import { IRasterLayerData, IRasterCfg, IRasterFileData } from '../interface';
import { bandsOperation } from '../utils/bandOperation/bands';
import { isNumberArray } from '../utils/util';
export default function rasterRgb(
data: IRasterLayerData,
cfg: IRasterCfg,
): IParserData {
const { extent, width, height, min, max, format, operation } = cfg;
let bandData, rasterWidth, rasterHeight;
if (format === undefined || isNumberArray(data)) {
// 兼容写法 - 用户直接传入解析完的波段数据
bandData = Array.from(data as number[]);
rasterWidth = width;
rasterHeight = height;
} else {
// 用户传入为解析的栅格数据 - arraybuffer
// 将数据统一为 IRasterFileData[]
const imageDataList = (Array.isArray(data)
? data
: [data]) as IRasterFileData[];
bandData = bandsOperation(imageDataList, format, operation);
}
const resultData = {
_id: 1,
dataArray: [
{
_id: 1,
data: bandData,
width: rasterWidth,
height: rasterHeight,
min,
max,
coordinates: [
[extent[0], extent[1]],
[extent[2], extent[3]],
],
},
],
};
return resultData;
}

View File

@ -33,6 +33,7 @@ function mergeCustomizer(objValue: any, srcValue: any) {
}
export default class Source extends EventEmitter implements ISource {
public type: string = 'source';
public inited: boolean = false;
public data: IParserData;
public center: [number, number];

View File

@ -0,0 +1,136 @@
import { IRasterFileData, IRasterFormat, IBandsOperation, IRgbOperation, IRasterData } from '../../interface';
import { calculate } from './math';
import {
ResponseCallback,
} from '@antv/l7-utils';
/**
* format + operation
* @param imageDataList
* @param rasterFormat
* @param operation
* @returns
*/
export async function bandsOperation(imageDataList: IRasterFileData[], rasterFormat: IRasterFormat, operation: IBandsOperation|undefined) {
if(imageDataList.length === 0) {
return {
rasterData: [0],
width: 1,
heigh: 1
}
}
let bandsData = (await Promise.all(
imageDataList.map(({ data, bands = [0] }) => rasterFormat(data, bands)),
)) as IRasterData[];
// @ts-ignore
bandsData = bandsData.flat();
// Tip: rasterFormat 返回值 rasterData|rasterData[]
// 多个栅格数据必须是相同大小才能进行相互之间的运算
const { width, height } = bandsData[0];
type IOperationResult = HTMLImageElement | Uint8Array | ImageBitmap | null | undefined
let rasterData: IOperationResult|IOperationResult[];
let channelR = undefined;
let channelG = undefined;
let channelB = undefined;
switch (typeof operation) {
case 'function':
rasterData = operation(bandsData) as Uint8Array;
break;
case 'object':
// 波段计算表达式 - operation
// operation: ['+', ['band', 0], 1]
/**
* operation: {
* r: ['+', ['band', 0], 1],
* g: ['+', ['band', 0], 1],
* b: ['+', ['band', 0], 1],
* }
*/
if(!Array.isArray(operation)) {
const rgbBands = getRgbBands(operation, bandsData);
const {
data,
channelRMax,
channelGMax,
channelBMax
} = combineRGBChannels(rgbBands);
rasterData = data as unknown as Uint8Array;
channelR = channelRMax;
channelG = channelGMax;
channelB = channelBMax;
} else {
rasterData = calculate(operation as any[], bandsData);
}
break;
default:
rasterData = bandsData[0].rasterData;
}
return {
rasterData,
width,
height,
channelR,
channelG,
channelB
}
}
function getRgbBands(operation: IRgbOperation, bandsData: IRasterData[]) {
if(operation.r === undefined) console.warn('Channel R lost in Operation! Use band[0] to fill!');
if(operation.g === undefined) console.warn('Channel G lost in Operation! Use band[0] to fill!');
if(operation.b === undefined) console.warn('Channel B lost in Operation! Use band[0] to fill!');
const r = calculate(operation.r || ['band', 0], bandsData);
const g = calculate(operation.g || ['band', 0], bandsData);
const b = calculate(operation.b || ['band', 0], bandsData);
return [r, g, b];
}
/**
*
* @param bandsData
*/
function combineRGBChannels(bandsData: Uint8Array[]) {
const channelR = bandsData[0];
const channelG = bandsData[1];
const channelB = bandsData[2];
const data = [];
let channelRMax = -Infinity;
let channelGMax = -Infinity;
let channelBMax = -Infinity;
for(let i = 0;i < channelR.length;i++) {
data.push(channelR[i]);channelRMax = Math.max(channelRMax, channelR[i]);
data.push((channelG)[i]);channelGMax = Math.max(channelGMax, channelR[i]);
data.push((channelB)[i]);channelBMax = Math.max(channelBMax, channelR[i]);
}
return {
data,
channelRMax,
channelGMax,
channelBMax
};
}
/**
*
*/
export async function handleRasterFiles(
rasterFiles: IRasterFileData[],
rasterFormat: IRasterFormat,
operation: IBandsOperation | undefined,
callback: ResponseCallback<any>
) {
const { rasterData, width, height } = await bandsOperation(rasterFiles, rasterFormat, operation)
// 目前 maxmin 没有生效
const defaultMIN = 0;
const defaultMAX = 8000;
callback(null, {
data: rasterData,
width,
height,
min: defaultMIN,
max: defaultMAX,
});
}

View File

@ -0,0 +1,133 @@
import { IRasterData } from '../../interface';
/**
* * * Math operators:
* `['*', value1, value2]` multiplies `value1` by `value2`
* `['/', value1, value2]` divides `value1` by `value2`
* `['+', value1, value2]` adds `value1` and `value2`
* `['-', value1, value2]` subtracts `value2` from `value1`
* `['%', value1, value2]` returns the result of `value1 % value2` (modulo)
* `['^', value1, value2]` returns the value of `value1` raised to the `value2` power
* `['abs', value1]` returns the absolute value of `value1`
* `['floor', value1]` returns the nearest integer less than or equal to `value1`
* `['round', value1]` returns the nearest integer to `value1`
* `['ceil', value1]` returns the nearest integer greater than or equal to `value1`
* `['sin', value1]` returns the sine of `value1`
* `['cos', value1]` returns the cosine of `value1`
* `['atan', value1, value2]` returns `atan2(value1, value2)`. If `value2` is not provided, returns `atan(value1)`
*/
export function mathematical(symbol: string, n1: number, n2: number) {
switch(symbol) {
case '+': return n1 + n2;
case '-': return n1 - n2;
case '*': return n1 * n2;
case '/': return n1 / n2;
case '%': return n1 % n2;
case '^': return Math.pow(n1, n2);
case 'abs': return Math.abs(n1);
case 'floor': return Math.floor(n1);
case 'round': return Math.round(n1);
case 'ceil': return Math.ceil(n1);
case 'sin': return Math.sin(n1);
case 'cos': return Math.cos(n1);
case 'atan': return (n2 === -1) ? Math.atan(n1): Math.atan2(n1, n2);
default:
console.warn('Calculate symbol err! Return default 0');
return 0;
}
}
/**
*
* @param express
* @param bandsData
*/
export function calculate(express: any[], bandsData: IRasterData[]) {
const {width, height} = bandsData[0];
const dataArray = bandsData.map(band => band.rasterData) as Uint8Array[];
const length = width * height;
const rasterData = [];
const originExp = JSON.stringify(express);
for(let i = 0;i < length; i++) {
const exp = JSON.parse(originExp);
// 将表达式中的 ['band', 0]、['band', 1] 等替换为实际的栅格数据
const expResult = spellExpress(exp, dataArray, i);
if(typeof expResult === 'number') {
// exp: ['band', 0] => exp: 2 ...
// exp 直接指定了波段值,替换完后直接就是数值了,无需计算
rasterData.push(expResult);
} else {
const result = calculateExpress(exp);
rasterData.push(result);
}
}
return rasterData as unknown as Uint8Array;
}
type IExpress = any[];
/**
*
* @param express
* @param dataArray
* @param index
*/
export function spellExpress(express: IExpress, dataArray: Uint8Array[], index: number) {
/**
*
*/
if(express.length === 2 && express[0] === 'band' && typeof express[1] === 'number') {
try {
return dataArray[express[1]][index];
} catch(err) {
console.warn('Raster Data err!');
return 0;
}
}
express.map((e, i) => {
if(Array.isArray(e) && e.length > 0) {
switch(e[0]) {
case 'band':
try {
express[i] = dataArray[e[1]][index];
} catch(err) {
console.warn('Raster Data err!');
express[i] = 0;
}
break;
default:
spellExpress(e, dataArray, index);
}
}
})
}
export function formatExpress(express: IExpress) {
const [symbol1, symbol2 = -1, symbol3 = -1] = express;
if(symbol1 === undefined) {
console.warn('Express err!')
return ['+', 0, 0];
}
const symbol = symbol1.replace(/\s+/g, '');
return [symbol, symbol2, symbol3];
}
export function calculateExpress(express: IExpress) {
const formatExp = formatExpress(express);
const str = formatExp[0];
let left = formatExp[1];
let right = formatExp[2];
if(Array.isArray(left)) {
left = calculateExpress(express[1]);
}
if(Array.isArray(right)) {
right = calculateExpress(express[2]);
}
return mathematical(str, left, right);
}

View File

@ -1,151 +0,0 @@
import {
getImage,
makeXMLHttpRequestPromise,
ResponseCallback,
IRasterParser,
arrayBufferToTiffImage,
RequestParameters,
getArrayBuffer,
getURLFromTemplate,
Tile,
TileLoadParams,
} from '@antv/l7-utils';
/**
* raster data tifflercdem
*
* @param url
* @param tileParams
* @param tile
* @param rasterParser
* @returns
*/
export const getTileBuffer = async (
url: string | string[],
tileParams: TileLoadParams,
tile: Tile,
rasterParser: (imageData: ArrayBuffer) => Promise<IRasterParser>,
): Promise<HTMLImageElement | ImageBitmap> => {
const requestParameters = {
url: getTileUrl(url, tileParams),
};
return new Promise((resolve, reject) => {
getTiffImage(
tile,
requestParameters,
(err, img) => {
if (err) {
reject(err);
} else if (img) {
resolve(img);
}
},
rasterParser,
);
});
};
const getTiffImage = async (
tile: Tile,
requestParameters: RequestParameters,
callback: ResponseCallback<HTMLImageElement | ImageBitmap | null>,
rasterParser: any,
) => {
if (Array.isArray(requestParameters.url)) {
const imageDataList = [];
const xhrList: any[] = [];
const errList = [];
const urls = requestParameters.url;
for (let i = 0; i < urls.length; i++) {
const params = {
...requestParameters,
url: urls[i],
};
const { err, data, xhr } = await makeXMLHttpRequestPromise({
...params,
type: 'arrayBuffer',
});
if (err) {
errList.push(err);
}
xhrList.push(xhr);
imageDataList.push(data);
}
setTileXHRCancelFunc(tile, xhrList);
if (errList.length > 0) {
callback(errList as Error[], null);
return;
}
const { rasterData, width, height } = await rasterParser(imageDataList);
const defaultMIN = 0;
const defaultMAX = 8000;
callback(null, {
// @ts-ignore
data: rasterData,
width,
height,
min: defaultMIN,
max: defaultMAX,
});
} else {
const xhr = getArrayBuffer(requestParameters, (err, imgData) => {
if (err) {
callback(err);
} else if (imgData) {
arrayBufferToTiffImage(imgData, callback, rasterParser);
}
});
setTileXHRCancelFunc(tile, [xhr]);
}
};
function setTileXHRCancelFunc(tile: Tile, xhrList: any[]) {
tile.xhrCancel = () => {
xhrList.map((xhr) => {
xhr.abort();
});
};
}
function getTileUrl(url: string | string[], tileParams: TileLoadParams) {
if (Array.isArray(url)) {
return url.map((src) => getURLFromTemplate(src, tileParams));
} else {
return getURLFromTemplate(url, tileParams);
}
}
export const getTileImage = async (
url: string | string[],
tileParams: TileLoadParams,
tile: Tile,
): Promise<HTMLImageElement | ImageBitmap> => {
// TODO: 后续考虑支持加载多服务
const imgUrl = getURLFromTemplate(
Array.isArray(url) ? url[0] : url,
tileParams,
);
return new Promise((resolve, reject) => {
const xhr = getImage({ url: imgUrl }, (err, img) => {
if (err) {
reject(err);
} else if (img) {
resolve(img);
}
});
tile.xhrCancel = () => xhr.abort();
});
};
export const defaultFormat = () => {
return {
rasterData: new Uint8Array([0]),
width: 1,
height: 1,
};
};

View File

@ -0,0 +1,82 @@
import { IRasterFormat, IBandsOperation, IRasterFileData } from '../../interface';
import { getTileBandParams, bindCancel } from './request';
import {
makeXMLHttpRequestPromise,
ResponseCallback,
ITileBand,
RequestParameters,
getArrayBuffer,
Tile,
} from '@antv/l7-utils';
import { handleRasterFiles } from '../bandOperation/bands';
export const getRasterFile = async (
tile: Tile,
requestParameters: RequestParameters,
callback: ResponseCallback<HTMLImageElement | ImageBitmap | null>,
rasterFormat: IRasterFormat,
operation?: IBandsOperation,
) => {
// Tip: 至少存在一个请求文件的 url处理得到标准的 ITileBand[] url 路径和 bands 参数
const tileBandParams: ITileBand[] = getTileBandParams(requestParameters.url);
if(tileBandParams.length > 1) {// 同时请求多文件
const { rasterFiles, xhrList, errList } = await getMultiArrayBuffer(tileBandParams, requestParameters);
bindCancel(tile, xhrList);
if (errList.length > 0) {
callback(errList as Error[], null);
return;
}
handleRasterFiles(rasterFiles, rasterFormat, operation, callback);
} else {
const xhr = getArrayBuffer(requestParameters, (err, imgData) => {
if (err) {
callback(err);
} else if (imgData) {
const rasterFiles = [{
data: imgData,
bands: tileBandParams[0].bands
}];
handleRasterFiles(rasterFiles, rasterFormat, operation, callback);
}
});
bindCancel(tile, [xhr]);
}
};
/**
* get multi raster files async
* @param tileBandParams
* @param requestParameters
* @returns
*/
async function getMultiArrayBuffer(tileBandParams: ITileBand[], requestParameters: RequestParameters) {
const rasterFiles: IRasterFileData[] = [];
const xhrList: any[] = [];
const errList = [];
for (let i = 0; i < tileBandParams.length; i++) {
const tileBandParam = tileBandParams[i]
const params = {
...requestParameters,
url: tileBandParam.url,
};
const bands = tileBandParam.bands;
const { err, data, xhr } = await makeXMLHttpRequestPromise({
...params,
type: 'arrayBuffer',
});
if (err) {
errList.push(err);
}
xhrList.push(xhr);
rasterFiles.push({
data,
bands,
});
}
return { rasterFiles, xhrList, errList }
}

View File

@ -0,0 +1,84 @@
import {
getImage,
ITileBand,
getURLFromTemplate,
Tile,
TileLoadParams,
} from '@antv/l7-utils';
import { getTileUrl } from './request';
import { IRasterFormat, IBandsOperation } from '../../interface';
import { getRasterFile } from './getRasterData';
/**
* raster data tifflercdem
*
* @param url
* @param tileParams
* @param tile
* @param rasterFormat
* @returns
*/
export const getTileBuffer = async (
url: string | string[] | ITileBand[],
tileParams: TileLoadParams,
tile: Tile,
rasterFormat: IRasterFormat,
operation?: IBandsOperation,
): Promise<HTMLImageElement | ImageBitmap> => {
const requestParameters = {
// getTileUrl 将原始的 url 路径进行转化(多服务器)
url: getTileUrl(url, tileParams),
};
return new Promise((resolve, reject) => {
getRasterFile(
tile,
requestParameters,
(err, img) => {
if (err) {
reject(err);
} else if (img) {
resolve(img);
}
},
rasterFormat,
operation,
);
});
};
/**
* jpgpng
* @param url
* @param tileParams
* @param tile
* @returns
*/
export const getTileImage = async (
url: string | string[],
tileParams: TileLoadParams,
tile: Tile,
): Promise<HTMLImageElement | ImageBitmap> => {
// TODO: 后续考虑支持加载多服务
const imgUrl = getURLFromTemplate(
Array.isArray(url) ? url[0] : url,
tileParams,
);
return new Promise((resolve, reject) => {
const xhr = getImage({ url: imgUrl }, (err, img) => {
if (err) {
reject(err);
} else if (img) {
resolve(img);
}
});
tile.xhrCancel = () => xhr.abort();
});
};
export const defaultFormat = () => {
return {
rasterData: new Uint8Array([0]),
width: 1,
height: 1,
};
};

View File

@ -0,0 +1,98 @@
import {
ITileBand,
getURLFromTemplate,
TileLoadParams,
Tile
} from '@antv/l7-utils';
/**
* url
* https://{a-c}.xxx.ccc => https://a.xxx.ccc
* https://{a-c}.xxx.ccc => https://b.xxx.ccc
* https://{a-c}.xxx.ccc => https://c.xxx.ccc
* @param url
* 'https://a.xxx.ccc '
* or
* ['https://a.xxx.ccc', 'https://c.ddd.ccc']
* or
* [
* {
* url: 'https://a.xxx.ccc',
* bands: [0]
* },
* ...
* ]
* @param tileParams
* @returns
*/
export function getTileUrl(
url: string | string[] | ITileBand[],
tileParams: TileLoadParams,
) {
if (Array.isArray(url)) {
if(typeof url[0] === 'string') {
return (url as string[]).map((src) =>
getURLFromTemplate(src, tileParams),
);
} else {
return (url as ITileBand[]).map((o) => {
return {
url: getURLFromTemplate(o.url, tileParams),
bands: o.bands || [0],
};
});
}
} else {
return getURLFromTemplate(url, tileParams);
}
}
/**
* url url
* @param urlBandParam
* 'https://a.bb.xxx'
* or
* [
* 'https://a.bb.xxx',
* 'https://a.bb.xxx'
* ]
* or
* [
* {
* url: 'https://a.bb.xxx',
* bands: [0, 1]
* },
* ...
* ]
* @returns
*/
export function getTileBandParams(urlBandParam: string | string[] | ITileBand[]): ITileBand[] {
if(typeof urlBandParam === 'string') {
return [{
url: urlBandParam,
bands: [0]
}]
} else if(typeof urlBandParam[0] === 'string') {
return urlBandParam.map(param => {
return {
url: param as string,
bands: [0]
};
})
} else {
return urlBandParam as ITileBand[];
}
}
/**
* tile
* @param tile
* @param xhrList
*/
export function bindCancel(tile: Tile, xhrList: any[]) {
tile.xhrCancel = () => {
xhrList.map((xhr) => {
xhr.abort();
});
};
}

View File

@ -1,8 +1,45 @@
import { IRasterLayerData, IRasterFileData } from '../interface';
interface IDataItem {
[key: string]: any;
}
export function getColumn(data: IDataItem[], columnName: string) {
return data.map((item: IDataItem) => {
return item[columnName] * 1;
});
}
export function isRasterFileData(data?: IRasterLayerData) {
if (data === undefined) return false;
if (!Array.isArray(data) && data.data !== undefined) {
return true;
} else {
return false;
}
}
export function isRasterFileDataArray(data: IRasterLayerData) {
if (Array.isArray(data)) {
if (data.length === 0) return false;
if (isRasterFileData(data[0] as IRasterFileData)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
export function isNumberArray(data: IRasterLayerData) {
if (Array.isArray(data)) {
if (data.length === 0) return true;
if (typeof data[0] === 'number') {
return true;
} else {
return false;
}
}
return false;
}

View File

@ -1,8 +1,13 @@
import { getReferrer } from './env';
import { $window, $XMLHttpRequest } from './mini-adapter';
export interface ITileBand {
url: string;
bands: number[];
}
export type RequestParameters = {
url: string | string[];
url: string | string[] | ITileBand[];
headers?: any;
method?: 'GET' | 'POST' | 'PUT';
body?: string;
@ -55,7 +60,7 @@ function makeFetchRequest(
callback: ResponseCallback<any>,
) {
const url = Array.isArray(requestParameters.url) ? requestParameters.url[0] : requestParameters.url;
const request = new Request(url, {
const request = new Request(url as string, {
method: requestParameters.method || 'GET',
body: requestParameters.body,
credentials: requestParameters.credentials,
@ -96,7 +101,7 @@ function makeFetchRequest(
new AJAXError(
response.status,
response.statusText,
url,
url.toString(),
body,
),
),
@ -157,7 +162,7 @@ function makeXMLHttpRequest(
type: xhr.getResponseHeader('Content-Type'),
});
callback(
new AJAXError(xhr.status, xhr.statusText, url, body),
new AJAXError(xhr.status, xhr.statusText, url.toString(), body),
);
}
};
@ -305,44 +310,3 @@ export const getImage = (
}
});
};
export const arrayBufferToTiffImage = async (
data: ArrayBuffer,
callback: (err?: Error | null, image?: any) => void,
rasterParser: any,
) => {
try {
const { rasterData, width, height } = await rasterParser(data);
const defaultMIN = 0;
const defaultMAX = 8000;
callback(null, {
data: rasterData,
width,
height,
min: defaultMIN,
max: defaultMAX,
});
} catch (err) {
callback(null, new Error('' + err));
}
};
export const getTiffImage = (
requestParameters: RequestParameters,
callback: ResponseCallback<HTMLImageElement | ImageBitmap | null>,
rasterParser: any,
) => {
return getArrayBuffer(requestParameters, (err, imgData) => {
if (err) {
callback(err);
} else if (imgData) {
arrayBufferToTiffImage(imgData, callback, rasterParser);
}
});
};
export interface IRasterParser {
rasterData: HTMLImageElement | ImageBitmap | null | undefined;
width: number;
height: number;
}