Skip to content

Commit

Permalink
feat(TIFFImageryProvider): enhance tile loading and reprojection perf…
Browse files Browse the repository at this point in the history
…ormance

- Fix multi render bands not found error.
- Added publishConfig to package.json for npm registry.
- Improved tile loading logic with error handling and timeout management.
- Optimized reprojection function with chunk processing for better performance on large datasets.
- Updated RGB rendering to utilize readSamples for dynamic band handling.
- Enhanced error handling and resource cleanup in asynchronous operations.
  • Loading branch information
hongfaqiu committed Jan 6, 2025
1 parent 0d1781f commit baea8c3
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 124 deletions.
5 changes: 4 additions & 1 deletion packages/TIFFImageryProvider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@
"tslib": "^2.5.0",
"typescript": "4.8.4"
},
"gitHead": "98a51f8d9e84194a1f48cab5cec43a0aa3c940f8"
"gitHead": "98a51f8d9e84194a1f48cab5cec43a0aa3c940f8",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
}
}
233 changes: 130 additions & 103 deletions packages/TIFFImageryProvider/src/TIFFImageryProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,115 +504,142 @@ export class TIFFImageryProvider {
* @param z
*/
private async _loadTile(reqx: number, reqy: number, reqz: number) {
let x = reqx, y = reqy, z = reqz, startX = reqx, startY = reqy;
const maxCogLevel = this.requestLevels.length - 1;
if (z > maxCogLevel) {
z = maxCogLevel;
x = x >> (reqz - maxCogLevel)
y = y >> (reqz - maxCogLevel)
startX = x << (reqz - z);
startY = y << (reqz - z);
}
try {
let x = reqx, y = reqy, z = reqz, startX = reqx, startY = reqy;
const maxCogLevel = this.requestLevels.length - 1;
if (z > maxCogLevel) {
z = maxCogLevel;
x = x >> (reqz - maxCogLevel)
y = y >> (reqz - maxCogLevel)
startX = x << (reqz - z);
startY = y << (reqz - z);
}

const index = this.requestLevels[z];
let image = this._images[index];
if (!image) {
image = this._images[index] = await this._source.getImage(index);
}
const index = this.requestLevels[z];
let image = this._images[index];
if (!image) {
image = this._images[index] = await this._source.getImage(index);
}

const width = image.getWidth();
const height = image.getHeight();
const tileXNum = this.tilingScheme.getNumberOfXTilesAtLevel(z);
const tileYNum = this.tilingScheme.getNumberOfYTilesAtLevel(z);
const tilePixel = {
xWidth: width / tileXNum,
yWidth: height / tileYNum
}
let window = [
Math.round(x * tilePixel.xWidth),
Math.round(y * tilePixel.yWidth),
Math.round((x + 1) * tilePixel.xWidth),
Math.round((y + 1) * tilePixel.yWidth),
];

if (this._proj && this.tilingScheme instanceof TIFFImageryProviderTilingScheme) {
const targetRect = this.tilingScheme.tileXYToNativeRectangle2(x, y, z);
const nativeRect = this.tilingScheme.nativeRectangle;
targetRect.west -= (nativeRect.width / width)
targetRect.east += (nativeRect.width / width)
targetRect.south -= (nativeRect.height / height)
targetRect.north += (nativeRect.height / height)

window = [
~~((targetRect.west - nativeRect.west) / nativeRect.width * width),
~~((nativeRect.north - targetRect.north) / nativeRect.height * height),
~~((targetRect.east - nativeRect.west) / nativeRect.width * width),
~~((nativeRect.north - targetRect.south) / nativeRect.height * height),
]
}
if (this.reverseY) {
window = [window[0], height - window[3], window[2], height - window[1]];
}
window = [window[0] - this._buffer, window[1] - this._buffer, window[2] + this._buffer, window[3] + this._buffer]
const sourceWidth = window[2] - window[0], sourceHeight = window[3] - window[1];
const width = image.getWidth();
const height = image.getHeight();
const tileXNum = this.tilingScheme.getNumberOfXTilesAtLevel(z);
const tileYNum = this.tilingScheme.getNumberOfYTilesAtLevel(z);
const tilePixel = {
xWidth: width / tileXNum,
yWidth: height / tileYNum
}
let window = [
Math.round(x * tilePixel.xWidth),
Math.round(y * tilePixel.yWidth),
Math.round((x + 1) * tilePixel.xWidth),
Math.round((y + 1) * tilePixel.yWidth),
];

if (this._proj && this.tilingScheme instanceof TIFFImageryProviderTilingScheme) {
const targetRect = this.tilingScheme.tileXYToNativeRectangle2(x, y, z);
const nativeRect = this.tilingScheme.nativeRectangle;
targetRect.west -= (nativeRect.width / width)
targetRect.east += (nativeRect.width / width)
targetRect.south -= (nativeRect.height / height)
targetRect.north += (nativeRect.height / height)

window = [
~~((targetRect.west - nativeRect.west) / nativeRect.width * width),
~~((nativeRect.north - targetRect.north) / nativeRect.height * height),
~~((targetRect.east - nativeRect.west) / nativeRect.width * width),
~~((nativeRect.north - targetRect.south) / nativeRect.height * height),
]
}
if (this.reverseY) {
window = [window[0], height - window[3], window[2], height - window[1]];
}
window = [window[0] - this._buffer, window[1] - this._buffer, window[2] + this._buffer, window[3] + this._buffer]
const sourceWidth = window[2] - window[0], sourceHeight = window[3] - window[1];

const options = {
window,
pool: this.geotiffWorkerPool,
samples: this.readSamples,
fillValue: this.noData,
interleave: false,
}

const options = {
window,
pool: this.geotiffWorkerPool,
samples: this.readSamples,
fillValue: this.noData,
interleave: false,
}
let res: TypedArrayArrayWithDimensions | TypedArray[];

let res: TypedArrayArrayWithDimensions | TypedArray[];
try {
if (this.renderOptions.convertToRGB) {
res = await image.readRGB(options) as TypedArrayArrayWithDimensions;
} else {
res = await image.readRasters(options) as TypedArrayArrayWithDimensions;
if (this.reverseY) {
res = await Promise.all((res as TypedArray[]).map((array) =>
reverseArray({ array, width: sourceWidth, height: sourceHeight })
)) as TypedArray[];
// 使用 AbortController 来控制异步操作
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000); // 30秒超时

try {
if (this.renderOptions.convertToRGB) {
res = await image.readRGB(options) as TypedArrayArrayWithDimensions;
} else {
res = await image.readRasters(options) as TypedArrayArrayWithDimensions;
if (this.reverseY) {
res = await Promise.all((res as TypedArray[]).map((array) =>
reverseArray({ array, width: sourceWidth, height: sourceHeight })
)) as TypedArray[];
}
}

if (this._proj?.project && this.tilingScheme instanceof TIFFImageryProviderTilingScheme) {
const sourceRect = this.tilingScheme.tileXYToNativeRectangle2(x, y, z);
const targetRect = this.tilingScheme.tileXYToRectangle(x, y, z);

const sourceBBox: BBox = [sourceRect.west, sourceRect.south, sourceRect.east, sourceRect.north];
const targetBBox = [targetRect.west, targetRect.south, targetRect.east, targetRect.north].map(CesiumMath.toDegrees) as BBox;

const result: TypedArray[] = [];
for (let i = 0; i < res.length; i++) {
try {
// 在循环中释放临时数据
const sourceData = res[i] as TypedArray;
const prjData = await reprojection({
data: sourceData,
sourceWidth,
sourceHeight,
nodata: this.noData,
project: this._proj.project,
sourceBBox,
targetBBox,
});
result.push(prjData);
} finally {
// 如果不是最后一个通道,释放源数据
if (i < res.length - 1 && res[i]) {
try {
(res[i] as TypedArray).fill(0);
res[i] = null;
} catch (e) {
console.warn('Failed to clean up channel data:', e);
}
}
}
}
res = result;
}
}

if (this._proj?.project && this.tilingScheme instanceof TIFFImageryProviderTilingScheme) {
const sourceRect = this.tilingScheme.tileXYToNativeRectangle2(x, y, z);
const targetRect = this.tilingScheme.tileXYToRectangle(x, y, z);

const sourceBBox: BBox = [sourceRect.west, sourceRect.south, sourceRect.east, sourceRect.north];
const targetBBox = [targetRect.west, targetRect.south, targetRect.east, targetRect.north].map(CesiumMath.toDegrees) as BBox;

const result: TypedArray[] = [];
for (let i = 0; i < res.length; i++) {
const prjData = reprojection({
data: res[i] as any,
sourceWidth,
sourceHeight,
nodata: this.noData,
project: this._proj.project,
sourceBBox,
targetBBox,
})
result.push(prjData)
const tileNum = 1 << (reqz - z)
const x0 = (reqx - startX) / tileNum;
const y0 = (reqy - startY) / tileNum;
const step = 1 / (1 << (reqz - z))
const x1 = x0 + step;
const y1 = y0 + step;

clearTimeout(timeoutId);
return {
data: res,
width: sourceWidth,
height: sourceHeight,
window: [x0, y0, x1, y1] as [number, number, number, number]
};
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Tile loading timeout');
}
res = result
throw error;
}

const tileNum = 1 << (reqz - z)
const x0 = (reqx - startX) / tileNum;
const y0 = (reqy - startY) / tileNum;
const step = 1 / (1 << (reqz - z))
const x1 = x0 + step;
const y1 = y0 + step;

return {
data: res,
width: sourceWidth,
height: sourceHeight,
window: [x0, y0, x1, y1] as [number, number, number, number]
};
} catch (error) {
this.errorEvent.raiseEvent(error);
throw error;
Expand Down Expand Up @@ -655,8 +682,8 @@ export class TIFFImageryProvider {

// Setup RGB rendering
targetPlot.removeAllDataset();
data.forEach((bandData, index) => {
targetPlot.addDataset(`band${index + 1}`, bandData, width, height);
this.readSamples.forEach((sample, index) => {
targetPlot.addDataset(`band${sample + 1}`, data[index], width, height);
});

targetPlot.setRGBOptions({
Expand Down
79 changes: 59 additions & 20 deletions packages/TIFFImageryProvider/src/helpers/reprojection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,80 @@ function inRange(val: number, range: [number, number]) {
}
}

export function reprojection(options: ReprojectionOptions): TypedArray {
// console.log(`[DEBUG] reprojection(${{...options, data: null }})`,options)
// 处理单个数据块
function processChunk(
options: ReprojectionOptions,
result: TypedArray,
startRow: number,
endRow: number,
targetWidth: number,
targetHeight: number
) {
const { data, sourceBBox, targetBBox, project, sourceWidth, sourceHeight, nodata } = options;
const { targetWidth = sourceWidth, targetHeight = sourceHeight } = options;

const [minX, minY, maxX, maxY] = sourceBBox;

const [minLon, minLat, maxLon, maxLat] = targetBBox;

const stepX = Math.abs(maxX - minX) / sourceWidth;
const stepY = Math.abs(maxY - minY) / sourceHeight;

const stepLon = Math.abs(maxLon - minLon) / targetWidth;
const stepLat = Math.abs(maxLat - minLat) / targetHeight;

const result = copyNewSize(data, targetWidth, targetHeight).fill(nodata)
// 使用批量处理优化内部循环
const colBatchSize = 100;
for (let i = startRow; i < endRow; i++) {
for (let jBatch = 0; jBatch < targetWidth; jBatch += colBatchSize) {
const endJ = Math.min(jBatch + colBatchSize, targetWidth);
for (let j = jBatch; j < endJ; j++) {
const lon = minLon + stepLon * (j + 0.5);
const lat = maxLat - stepLat * (i + 0.5);
const [x, y] = project([lon, lat]);

for (let i = 0; i < targetHeight; i++) {
for (let j = 0; j < targetWidth; j++) {
const lon = minLon + stepLon * (j + 0.5);
const lat = maxLat - stepLat * (i + 0.5);
const [x, y] = project([lon, lat]);
if (!inRange(x, [minX, maxX]) || !inRange(y, [minY, maxY])) {
result[i * targetWidth + j] = nodata;
continue;
}

if (!inRange(x, [minX, maxX]) || !inRange(y, [minX, maxY])) {
break;
const indexX = ~~((x - minX) / stepX);
const indexY = ~~((maxY - y) / stepY);
result[i * targetWidth + j] = data[indexY * sourceWidth + indexX];
}
}
}
}

export async function reprojection(options: ReprojectionOptions): Promise<TypedArray> {
const { targetWidth = options.sourceWidth, targetHeight = options.sourceHeight } = options;
const result = copyNewSize(options.data, targetWidth, targetHeight).fill(options.nodata);

const indexX = ~~((x - minX) / stepX);
const indexY = ~~((maxY - y) / stepY);
// 如果数据量较小,使用同步处理
if (targetHeight * targetWidth < 10000) {
processChunk(options, result, 0, targetHeight, targetWidth, targetHeight);
return result;
}

const sourceVal = data[indexY * sourceWidth + indexX];
const index = i * targetWidth + j;
// 计算最佳的块大小
const CHUNK_SIZE = Math.min(100, Math.max(10, Math.floor(targetHeight / 10)));

result[index] = sourceVal;
try {
// 分块处理
for (let startRow = 0; startRow < targetHeight; startRow += CHUNK_SIZE) {
const endRow = Math.min(startRow + CHUNK_SIZE, targetHeight);
await new Promise<void>((resolve) => {
requestAnimationFrame(() => {
processChunk(options, result, startRow, endRow, targetWidth, targetHeight);
resolve();
});
});
}
return result;
} finally {
// 在 finally 块中清理源数据,确保无论成功失败都会执行
if (options.data !== result) {
try {
options.data.fill(0);
} catch (e) {
console.warn('Failed to clean up source data:', e);
}
}
}
return result;
}

0 comments on commit baea8c3

Please sign in to comment.