diff --git a/packages/TIFFImageryProvider/README.md b/packages/TIFFImageryProvider/README.md index 3fc6cb5..358d4a3 100644 --- a/packages/TIFFImageryProvider/README.md +++ b/packages/TIFFImageryProvider/README.md @@ -170,8 +170,8 @@ interface TIFFImageryProviderOptions { } | undefined; /** cache survival time, defaults to 60 * 1000 ms */ cache?: number; - /** geotiff resample method, defaults to nearest */ - resampleMethod?: 'nearest' | 'bilinear' | 'linear'; + /** resample web worker pool size, defaults to the number of CPUs available. When this parameter is `null` or 0, then the resampling will be done in the main thread. */ + workerPoolSize?: number; } type TIFFImageryProviderRenderOptions = { diff --git a/packages/TIFFImageryProvider/README_CN.md b/packages/TIFFImageryProvider/README_CN.md index dc4ebce..25bcf86 100644 --- a/packages/TIFFImageryProvider/README_CN.md +++ b/packages/TIFFImageryProvider/README_CN.md @@ -55,7 +55,7 @@ provider.readyPromise.then(() => { }) ``` -**[实验性]** 如果 TIFF 的投影不是 EPSG:4326,你可以通过 ``projFunc`` 来处理投影 +**[实验性]** 如果 TIFF 的投影不是 EPSG:4326或EPSG:3857,你可以通过 ``projFunc`` 来处理投影 ```ts import proj4 from 'proj4'; @@ -166,8 +166,8 @@ interface TIFFImageryProviderOptions { } | undefined; /** 缓存生存时间,默认为60 * 1000毫秒 */ cache?: number; - /** geotiff 重采样方法, 默认为 nearest */ - resampleMethod?: 'nearest' | 'bilinear' | 'linear'; + /** 重新采样 Web Worker 工作池大小,默认为可用 CPU 数量。当该参数为null或 0,则重采样将在主线程中完成。 */ + workerPoolSize?: number; } type TIFFImageryProviderRenderOptions = { diff --git a/packages/TIFFImageryProvider/package.json b/packages/TIFFImageryProvider/package.json index 9df06b8..19051ff 100644 --- a/packages/TIFFImageryProvider/package.json +++ b/packages/TIFFImageryProvider/package.json @@ -48,6 +48,7 @@ "rollup": "^3.28.1", "rollup-plugin-dts": "^6.0.1", "rollup-plugin-esbuild": "^5.0.0", + "rollup-plugin-web-worker-loader": "^1.6.1", "tslib": "^2.5.0", "typescript": "4.8.4" }, diff --git a/packages/TIFFImageryProvider/rollup.config.js b/packages/TIFFImageryProvider/rollup.config.js index 016d0ff..adcf968 100644 --- a/packages/TIFFImageryProvider/rollup.config.js +++ b/packages/TIFFImageryProvider/rollup.config.js @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import esbuild from 'rollup-plugin-esbuild'; import path from 'path'; import dts from 'rollup-plugin-dts'; +import webWorkerLoader from "rollup-plugin-web-worker-loader"; const pkg = JSON.parse( readFileSync(new URL('./package.json', import.meta.url)).toString(), @@ -27,6 +28,9 @@ const config = [ esbuild({ target: 'node14', }), + webWorkerLoader({ + extensions: ["ts", "js"], + }), ] }, { diff --git a/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts b/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts index c490cd4..db174af 100644 --- a/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts +++ b/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts @@ -2,13 +2,15 @@ import { Event, GeographicTilingScheme, Credit, Rectangle, ImageryLayerFeatureIn import GeoTIFF, { Pool, fromUrl, fromBlob, GeoTIFFImage } from 'geotiff'; import { addColorScale, plot } from './plotty' -import { getMinMax, generateColorScale, findAndSortBandNumbers, stringColorToRgba } from "./helpers/utils"; +import { getMinMax, generateColorScale, findAndSortBandNumbers, stringColorToRgba, resampleData } from "./helpers/utils"; import { ColorScaleNames, TypedArray } from "./plotty/typing"; import TIFFImageryProviderTilingScheme from "./TIFFImageryProviderTilingScheme"; import { reprojection } from "./helpers/reprojection"; import { GenerateImageOptions, generateImage } from "./helpers/generateImage"; import { reverseArray } from "./helpers/utils"; +import { createCanavas } from "./helpers/createCanavas"; +import WorkerPool from "./worker/pool"; export interface SingleBandRenderOptions { /** band index start from 1, defaults to 1 */ @@ -136,10 +138,12 @@ export interface TIFFImageryProviderOptions { } | undefined; /** cache survival time, defaults to 60 * 1000 ms */ cache?: number; - /** geotiff resample method, defaults to nearest */ - resampleMethod?: 'nearest' | 'bilinear' | 'linear'; + /** resample web worker pool size, defaults to the number of CPUs available. When this parameter is `null` or 0, then the resampling will be done in the main thread. */ + workerPoolSize?: number; } -const canvas = document.createElement('canvas'); + +const canvas = createCanavas(256, 256); + let workerPool: Pool; function getWorkerPool() { if (!workerPool) { @@ -166,7 +170,7 @@ export class TIFFImageryProvider { }>; noData: number; hasAlphaChannel: boolean; - plot: plot; + plot?: plot; renderOptions: TIFFImageryProviderRenderOptions; readSamples: number[]; requestLevels: number[]; @@ -177,7 +181,7 @@ export class TIFFImageryProvider { private _images: (GeoTIFFImage | null)[] = []; private _imagesCache: Record = {}; private _cacheTime: number; private _isTiled: boolean; @@ -190,6 +194,7 @@ export class TIFFImageryProvider { origin: number[]; reverseY: boolean = false; samples: number; + workerPool: WorkerPool; constructor(private readonly options: TIFFImageryProviderOptions & { /** @@ -206,6 +211,7 @@ export class TIFFImageryProvider { this.credit = new Credit(options.credit || "", false); this.errorEvent = new Event(); this._cacheTime = options.cache ?? 60 * 1000; + this.workerPool = new WorkerPool(options.workerPoolSize); this.ready = false; if (defined(options.url)) { @@ -272,12 +278,10 @@ export class TIFFImageryProvider { this.rectangle.east += CesiumMath.TWO_PI; } this._imageCount = await source.getImageCount(); - this.tileSize = this.tileWidth = tileSize || (this._isTiled ? image.getTileWidth() : image.getWidth()) || 512; - this.tileHeight = tileSize || (this._isTiled ? image.getTileHeight() : image.getHeight()) || 512; + this.tileSize = this.tileWidth = tileSize || (this._isTiled ? image.getTileWidth() : image.getWidth()) || 256; + this.tileHeight = tileSize || (this._isTiled ? image.getTileHeight() : image.getHeight()) || 256; // 获取合适的COG层级 this.requestLevels = this._isTiled ? await this._getCogLevels() : [0]; - const maxCogLevel = this.requestLevels.length - 1 - this.maximumLevel = this.maximumLevel > maxCogLevel ? maxCogLevel : this.maximumLevel; this._images = new Array(this._imageCount).fill(null); // 获取波段数 @@ -383,7 +387,7 @@ export class TIFFImageryProvider { this.plot = new plot({ canvas, ...single, - domain + domain, }) this.plot.setNoDataValue(this.noData); @@ -397,6 +401,7 @@ export class TIFFImageryProvider { this.plot.setColorScale(single?.colorScale ?? 'blackwhite'); } } + } catch (e) { console.error(e); this.errorEvent.raiseEvent(e); @@ -458,7 +463,7 @@ export class TIFFImageryProvider { const height = image.getHeight(); const size = Math.max(width, height); - // 如果第一张瓦片的image tileSize大于512,则顺位后延,以减少请求量 + // 如果第一张瓦片的image tileSize大于256,则顺位后延,以减少请求量 if (i === this._imageCount - 1) { const firstImageLevel = Math.ceil((size - this.tileSize) / this.tileSize) levels.push(...new Array(firstImageLevel).fill(i)) @@ -483,7 +488,17 @@ export class TIFFImageryProvider { * @param y * @param z */ - private async _loadTile(x: number, y: number, z: number) { + 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); + } + const index = this.requestLevels[z]; let image = this._images[index]; if (!image) { @@ -523,27 +538,26 @@ export class TIFFImageryProvider { if (this.reverseY) { window = [window[0], height - window[3], window[2], height - window[1]]; } + const windowWidth = window[2] - window[0], windowHeight = window[3] - window[1]; + const options = { window, pool: getWorkerPool(), - width: this.tileWidth, - height: this.tileHeight, samples: this.readSamples, - resampleMethod: this.options.resampleMethod, fillValue: this.noData, interleave: false, } - let res: TypedArray[]; + let res: any; try { if (this.renderOptions.convertToRGB) { - res = await image.readRGB(options) as TypedArray[]; + res = await image.readRGB(options); } else { - res = await image.readRasters(options) as TypedArray[]; + res = await image.readRasters(options); if (this.reverseY) { - res = await Promise.all((res).map((arr: any) => reverseArray({ array: arr, width: (res as any).width, height: (res as any).height }))) as any; + res = await Promise.all((res).map((arr) => reverseArray({ array: arr, width: res.width, height: res.height }))) as any; } } - + if (this._proj?.project && this.tilingScheme instanceof TIFFImageryProviderTilingScheme) { const sourceRect = this.tilingScheme.tileXYToNativeRectangle2(x, y, z); const targetRect = this.tilingScheme.tileXYToRectangle(x, y, z); @@ -555,10 +569,8 @@ export class TIFFImageryProvider { for (let i = 0; i < res.length; i++) { const prjData = reprojection({ data: res[i] as any, - sourceWidth: this.tileWidth, - sourceHeight: this.tileHeight, - targetWidth: this.tileWidth, - targetHeight: this.tileHeight, + sourceWidth: windowWidth, + sourceHeight: windowHeight, nodata: this.noData, project: this._proj.project, sourceBBox, @@ -567,8 +579,24 @@ export class TIFFImageryProvider { result.push(prjData) } res = result - } + + 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; + + res = await Promise.all(res.map(async (data: any) => this.workerPool.resample(data as any, { + sourceWidth: windowWidth, + sourceHeight: windowHeight, + targetWidth: this.tileWidth, + targetHeight: this.tileHeight, + window: [x0, y0, x1, y1] + }) + )); + return { data: res, width: this.tileWidth, @@ -580,14 +608,6 @@ export class TIFFImageryProvider { } } - private _createTile() { - const canv = document.createElement("canvas"); - canv.width = this.tileWidth; - canv.height = this.tileHeight; - canv.style.imageRendering = "pixelated"; - return canv; - } - async requestImage( x: number, y: number, @@ -605,11 +625,12 @@ export class TIFFImageryProvider { try { const { width, height, data } = await this._loadTile(x, y, z); - if (this._destroyed) { + + if (this._destroyed || !width || !height) { return undefined; } - let result: ImageData | HTMLImageElement | HTMLCanvasElement + let result: ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas if (multi || convertToRGB) { const opts: GenerateImageOptions = { @@ -643,8 +664,8 @@ export class TIFFImageryProvider { this.plot.renderDataset(`b${band}`) } - const canv = this._createTile() - const ctx = canv.getContext("2d") + const canv = createCanavas(this.tileWidth, this.tileHeight) + const ctx = canv.getContext("2d") as CanvasRenderingContext2D ctx.drawImage(this.plot.canvas, 0, 0); result = canv; } @@ -728,6 +749,7 @@ export class TIFFImageryProvider { this._source = undefined; this._imagesCache = undefined; this.plot?.destroy(); + this.workerPool.destroy(); this._destroyed = true; } } diff --git a/packages/TIFFImageryProvider/src/helpers/createCanavas.ts b/packages/TIFFImageryProvider/src/helpers/createCanavas.ts new file mode 100644 index 0000000..c44ce27 --- /dev/null +++ b/packages/TIFFImageryProvider/src/helpers/createCanavas.ts @@ -0,0 +1,10 @@ +export function createCanavas(width: number, height: number) { + if ('OffscreenCanvas' in window) { + return new OffscreenCanvas(width, height); + } else { + const canv = document.createElement("canvas"); + canv.width = width; + canv.height = height; + return canv; + } +} \ No newline at end of file diff --git a/packages/TIFFImageryProvider/src/helpers/utils.ts b/packages/TIFFImageryProvider/src/helpers/utils.ts index 5b2b21f..ab5f503 100644 --- a/packages/TIFFImageryProvider/src/helpers/utils.ts +++ b/packages/TIFFImageryProvider/src/helpers/utils.ts @@ -108,4 +108,30 @@ export function reverseArray(options: { } return reversedArray; +} + +export type ReasmpleDataOptions = { + sourceWidth: number; + sourceHeight: number; + targetWidth: number; + targetHeight: number; + /** start from 0 to 1, examples: [0, 0, 0.5, 0.5] */ + window: [number, number, number, number]; +} + +export function resampleData(data: Uint8Array | Int16Array | Int32Array, options: ReasmpleDataOptions) { + const { sourceWidth, sourceHeight, targetWidth, targetHeight, window } = options; + const [x0, y0, x1, y1] = window; + + const resampledData = new Array(targetWidth * targetHeight); + + for (let y = 0; y < targetHeight; y++) { + for (let x = 0; x < targetWidth; x++) { + const col = (sourceWidth * (x0 + x / targetWidth * (x1 - x0))) >>> 0; + const row = (sourceHeight * (y0 + y / targetHeight * (y1 - y0))) >>> 0; + resampledData[y * targetWidth + x] = data[row * sourceWidth + col]; + } + } + + return resampledData; } \ No newline at end of file diff --git a/packages/TIFFImageryProvider/src/plotty/index.ts b/packages/TIFFImageryProvider/src/plotty/index.ts index 9eec29a..e89351e 100644 --- a/packages/TIFFImageryProvider/src/plotty/index.ts +++ b/packages/TIFFImageryProvider/src/plotty/index.ts @@ -18,7 +18,7 @@ function hasOwnProperty(obj: any, prop: string) { function defaultFor(arg: any, val: any) { return typeof arg !== 'undefined' ? arg : val; } -function create3DContext(canvas: HTMLCanvasElement, optAttribs: { premultipliedAlpha: boolean; }) { +function create3DContext(canvas: HTMLCanvasElement | OffscreenCanvas, optAttribs: { premultipliedAlpha: boolean; }) { const names = ['webgl', 'experimental-webgl']; let context: WebGLRenderingContext | null= null; for (let ii = 0; ii < names.length; ++ii) { @@ -259,7 +259,7 @@ void main() { * */ class plot { - canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement | OffscreenCanvas; currentDataset: DataSet; datasetCollection: Record; gl: WebGLRenderingContext | null; @@ -313,10 +313,10 @@ class plot { gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); } else { // Fall back to 2d context - this.ctx = this.canvas.getContext('2d'); + this.ctx = (this.canvas as HTMLCanvasElement).getContext('2d'); } } else { - this.ctx = this.canvas.getContext('2d'); + this.ctx = (this.canvas as HTMLCanvasElement).getContext('2d'); } if (options.colorScaleImage) { @@ -474,9 +474,9 @@ class plot { /** * Set the canvas to draw to. When no canvas is supplied, a new canvas element * is created. - * @param {HTMLCanvasElement} [canvas] the canvas element to render to. + * @param {HTMLCanvasElement | OffscreenCanvas} [canvas] the canvas element to render to. */ - setCanvas(canvas: HTMLCanvasElement) { + setCanvas(canvas: HTMLCanvasElement | OffscreenCanvas) { this.canvas = canvas || document.createElement('canvas'); } @@ -513,7 +513,7 @@ class plot { * Get the canvas that is currently rendered to. * @returns {HTMLCanvasElement} the canvas that is currently rendered to. */ - getCanvas(): HTMLCanvasElement { + getCanvas(): HTMLCanvasElement | OffscreenCanvas { return this.canvas; } diff --git a/packages/TIFFImageryProvider/src/plotty/typing.ts b/packages/TIFFImageryProvider/src/plotty/typing.ts index d5cd8e8..aa48953 100644 --- a/packages/TIFFImageryProvider/src/plotty/typing.ts +++ b/packages/TIFFImageryProvider/src/plotty/typing.ts @@ -19,7 +19,7 @@ export type PlotOptions = { /** * The canvas to render to. */ - canvas?: HTMLCanvasElement; + canvas?: HTMLCanvasElement | OffscreenCanvas; /** * The raster data to render. diff --git a/packages/TIFFImageryProvider/src/utils/xyzTile.ts b/packages/TIFFImageryProvider/src/utils/xyzTile.ts index 53e8a2d..c9a3ac7 100644 --- a/packages/TIFFImageryProvider/src/utils/xyzTile.ts +++ b/packages/TIFFImageryProvider/src/utils/xyzTile.ts @@ -1,6 +1,6 @@ const debugCanvas = document.createElement('canvas'); -debugCanvas.width = 512; -debugCanvas.height = 512; +debugCanvas.width = 256; +debugCanvas.height = 256; export function generateCanvasWithText(text: string) { try { diff --git a/packages/TIFFImageryProvider/src/worker/pool.ts b/packages/TIFFImageryProvider/src/worker/pool.ts new file mode 100644 index 0000000..f8dd547 --- /dev/null +++ b/packages/TIFFImageryProvider/src/worker/pool.ts @@ -0,0 +1,70 @@ +import { ReasmpleDataOptions, resampleData } from '../helpers/utils'; +// @ts-ignore +import create from 'web-worker:./worker'; + +const defaultPoolSize = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency || 2) : 2; + +/** + * @module pool + */ + +/** + * Pool for workers to decode chunks of the images. + */ +class WorkerPool { + workers: null | { + worker: any; + idle: boolean; + }[]; + size: number; + messageId: number; + /** + * @constructor + * @param {Number} [size] The size of the pool. Defaults to the number of CPUs + * available. When this parameter is `null` or 0, then the + * decoding will be done in the main thread. + */ + constructor(size: number = defaultPoolSize) { + this.workers = null; + this.size = size ?? 0; + this.messageId = 0; + if (size) { + this.workers = []; + for (let i = 0; i < size; i++) { + this.workers.push({ worker: create(), idle: true }); + } + } + } + + async resample(data: Uint8Array | Int16Array | Int32Array, options: ReasmpleDataOptions): Promise { + return this.size === 0 + ? resampleData(data, options) + : new Promise((resolve) => { + const worker = this.workers.find((candidate) => candidate.idle) + || this.workers[Math.floor(Math.random() * this.size)]; + worker.idle = false; + const id = this.messageId++; + const onMessage = (e) => { + if (e.data.id === id) { + worker.idle = true; + resolve(e.data.data); + worker.worker.removeEventListener('message', onMessage); + } + }; + worker.worker.addEventListener('message', onMessage); + + worker.worker.postMessage({ data, options, id }); + }); + } + + destroy() { + if (this.workers) { + this.workers.forEach((worker) => { + worker.worker.terminate(); + }); + this.workers = null; + } + } +} + +export default WorkerPool; \ No newline at end of file diff --git a/packages/TIFFImageryProvider/src/worker/worker.ts b/packages/TIFFImageryProvider/src/worker/worker.ts new file mode 100644 index 0000000..29710f5 --- /dev/null +++ b/packages/TIFFImageryProvider/src/worker/worker.ts @@ -0,0 +1,7 @@ +import { resampleData } from "../helpers/utils"; + +onmessage = function (e) { + const { data, options, id } = e.data; + const result = resampleData(data, options); + postMessage({ data: result, id }); +}; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 584ab20..1a3acff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: rollup-plugin-esbuild: specifier: ^5.0.0 version: 5.0.0(esbuild@0.19.2)(rollup@3.28.1) + rollup-plugin-web-worker-loader: + specifier: ^1.6.1 + version: 1.6.1(rollup@3.28.1) tslib: specifier: ^2.5.0 version: 2.5.0 @@ -7220,6 +7223,14 @@ packages: rollup: 2.79.1 dev: true + /rollup-plugin-web-worker-loader@1.6.1(rollup@3.28.1): + resolution: {integrity: sha512-4QywQSz1NXFHKdyiou16mH3ijpcfLtLGOrAqvAqu1Gx+P8+zj+3gwC2BSL/VW1d+LW4nIHC8F7d7OXhs9UdR2A==} + peerDependencies: + rollup: ^1.9.2 || ^2.0.0 + dependencies: + rollup: 3.28.1 + dev: true + /rollup@2.79.1: resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} engines: {node: '>=10.0.0'} diff --git a/vite-example/src/index.ts b/vite-example/src/index.ts index 7c41362..91bd776 100644 --- a/vite-example/src/index.ts +++ b/vite-example/src/index.ts @@ -20,7 +20,7 @@ const viewer = new Viewer('cesiumContainer', { orderIndependentTranslucency: false, }); -const provider = await TIFFImageryProvider.fromUrl('/cogtif.tif', { +TIFFImageryProvider.fromUrl('/cogtif.tif', { enablePickFeatures: true, projFunc: (code) => { if (![4326, 3857, 900913].includes(code)) { @@ -39,17 +39,18 @@ const provider = await TIFFImageryProvider.fromUrl('/cogtif.tif', { return undefined }, renderOptions: { - "single": { - colorScale: "rainbow", + single: { + colorScale: 'rainbow' } - } -}); -console.log(provider); -const imageryLayer = viewer.imageryLayers.addImageryProvider(provider as any); -const legend = document.getElementById("legend") as HTMLImageElement; -const img = provider.plot.colorScaleCanvas.toDataURL(); -legend.src = img; - -viewer.flyTo(imageryLayer, { - duration: 1, -}); \ No newline at end of file + }, +}).then((provider) => { + console.log(provider); + const imageryLayer = viewer.imageryLayers.addImageryProvider(provider as any); + const legend = document.getElementById("legend") as HTMLImageElement; + const img = provider.plot?.colorScaleCanvas.toDataURL(); + legend.src = img; + + viewer.flyTo(imageryLayer, { + duration: 1, + }); +}) \ No newline at end of file