UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

251 lines (228 loc) 8.93 kB
import * as THREE from 'three'; /** * @typedef {Object} GeoTIFFLevel * @property {GeoTIFFImage} image * @property {number} width * @property {number} height * @property {number[]} resolution */ /** * Select the best overview level (or the final image) to match the * requested extent and pixel width and height. * * @param {Object} source The COGSource * @param {Extent} source.extent Source extent * @param {GeoTIFFLevel[]} source.levels * @param {THREE.Vector2} source.dimensions * @param {Extent} requestExtent The node extent. * @param {number} requestWidth The pixel width of the window. * @param {number} requestHeight The pixel height of the window. * @returns {GeoTIFFLevel} The selected zoom level. */ function selectLevel(source, requestExtent, requestWidth, requestHeight) { // Dimensions of the requested extent const extentDimension = requestExtent.clone().planarDimensions(); const targetResolution = Math.min( extentDimension.x / requestWidth, extentDimension.y / requestHeight, ); let level; // Select the image with the best resolution for our needs for (let index = source.levels.length - 1; index >= 0; index--) { level = source.levels[index]; const sourceResolution = Math.min( source.dimensions.x / level.width, source.dimensions.y / level.height, ); if (targetResolution >= sourceResolution) { break; } } return level; } /** * Returns a window in the image's coordinates that matches the requested extent. * * @param {Object} source The COGSource * @param {number[]} source.origin Root image origin as an XYZ-vector * @param {Extent} extent The window extent. * @param {number[]} resolution The spatial resolution of the window. * @returns {number[]} The window. */ function makeWindowFromExtent(source, extent, resolution) { const [oX, oY] = source.origin; const [imageResX, imageResY] = resolution; const wnd = [ Math.round((extent.west - oX) / imageResX), Math.round((extent.north - oY) / imageResY), Math.round((extent.east - oX) / imageResX), Math.round((extent.south - oY) / imageResY), ]; const xMin = Math.min(wnd[0], wnd[2]); let xMax = Math.max(wnd[0], wnd[2]); const yMin = Math.min(wnd[1], wnd[3]); let yMax = Math.max(wnd[1], wnd[3]); // prevent zero-sized requests if (Math.abs(xMax - xMin) === 0) { xMax += 1; } if (Math.abs(yMax - yMin) === 0) { yMax += 1; } return [xMin, yMin, xMax, yMax]; } /** * Reads raster data from the image as RGB. * The result is always an interleaved typed array. * Colorspaces other than RGB will be transformed to RGB, color maps expanded. * * @param {Source} source The COGSource. * @param {GeoTIFFLevel} level The GeoTIFF level to read * @param {number[]} viewport The image region to read. * @returns {Promise<TypedArray[]>} The raster data */ async function readRGB(source, level, viewport) { try { // TODO possible optimization: instead of letting geotiff.js crop and resample // the tiles into the desired region, we could use image.getTileOrStrip() to // read individual tiles (aka blocks) and make a texture per block. This way, // there would not be multiple concurrent reads for the same block, and we would not // waste time resampling the blocks since resampling is already done in the composer. // We would create more textures, but it could be worth it. return await level.image.readRGB({ window: viewport, pool: source.pool, width: source.tileWidth, height: source.tileHeight, resampleMethod: source.resampleMethod, enableAlpha: true, interleave: true, }); } catch (error) { if (error.toString() === 'AggregateError: Request failed') { // Problem with the source that is blocked by another fetch // (request failed in readRasters). See the conversations in // https://github.com/geotiffjs/geotiff.js/issues/218 // https://github.com/geotiffjs/geotiff.js/issues/221 // https://github.com/geotiffjs/geotiff.js/pull/224 // Retry until it is not blocked. // TODO retry counter await new Promise((resolve) => { setTimeout(resolve, 100); }); return readRGB(level, viewport, source); } throw error; } } /** * Creates a texture from the pixel buffer * * @param {Object} source The COGSource * @param {THREE.TypedArray[]} buffer The pixel buffer * @param {number} buffer.width * @param {number} buffer.height * @param {number} buffer.byteLength * @returns {THREE.DataTexture} The generated texture. */ function createTexture(source, buffer) { const { byteLength } = buffer; const width = source.tileWidth; const height = source.tileHeight; const pixelCount = width * height; const targetDataType = source.dataType; const format = THREE.RGBAFormat; const channelCount = 4; const isRGBA = pixelCount * channelCount === byteLength; let tmpBuffer = buffer; switch (targetDataType) { case THREE.UnsignedByteType: { if (!isRGBA) { tmpBuffer = convertToRGBA(tmpBuffer, new Uint8ClampedArray(pixelCount * channelCount), source.defaultAlpha); } return new THREE.DataTexture(tmpBuffer, width, height, format, THREE.UnsignedByteType); } case THREE.FloatType: { if (!isRGBA) { tmpBuffer = convertToRGBA(tmpBuffer, new Float32Array(pixelCount * channelCount), source.defaultAlpha / 255); } return new THREE.DataTexture(tmpBuffer, width, height, format, THREE.FloatType); } default: throw new Error('unsupported data type'); } } /** * Convert RGB pixel buffer to RGBA pixel buffer * * @param {THREE.TypedArray[]} buffer The RGB pixel buffer * @param {THREE.TypedArray[]} newBuffer The empty RGBA pixel buffer * @param {number} defaultAlpha Default alpha value * @returns {THREE.DataTexture} The generated texture. */ function convertToRGBA(buffer, newBuffer, defaultAlpha) { const { width, height } = buffer; for (let i = 0; i < width * height; i++) { const oldIndex = i * 3; const index = i * 4; // Copy RGB from original buffer newBuffer[index + 0] = buffer[oldIndex + 0]; // R newBuffer[index + 1] = buffer[oldIndex + 1]; // G newBuffer[index + 2] = buffer[oldIndex + 2]; // B // Add alpha to new buffer newBuffer[index + 3] = defaultAlpha; // A } return newBuffer; } /** * The COGParser module provides a [parse]{@link module:COGParser.parse} * method that takes a COG in and gives a `THREE.DataTexture` that can be * displayed in the view. * * It needs the [geotiff](https://github.com/geotiffjs/geotiff.js/) library to parse the * COG. * * @example * GeoTIFF.fromUrl('http://image.tif') * .then(COGParser.parse) * .then(function _(texture) { * var source = new itowns.FileSource({ features: texture }); * var layer = new itowns.ColorLayer('cog', { source }); * view.addLayer(layer); * }); * * @module COGParser */ const COGParser = (function _() { return { /** * Parse a COG file and return a `THREE.DataTexture`. * * @param {Object} data Data passed with the Tile extent * @param {Extent} data.extent * @param {Object} options Options (contains source) * @param {Object} options.in * @param {COGSource} options.in.source * @param {number} options.in.tileWidth * @param {number} options.in.tileHeight * @return {Promise<THREE.DataTexture>} A promise resolving with a `THREE.DataTexture`. * * @memberof module:COGParser */ parse: async function _(data, options) { const source = options.in; const tileExtent = options.extent.isExtent ? options.extent.as(source.crs) : options.extent.toExtent(source.crs); const level = selectLevel(source, tileExtent, source.tileWidth, source.tileHeight); const viewport = makeWindowFromExtent(source, tileExtent, level.resolution); const rgbBuffer = await readRGB(source, level, viewport); const texture = createTexture(source, rgbBuffer); texture.flipY = true; texture.extent = options.extent; texture.needsUpdate = true; texture.magFilter = THREE.LinearFilter; texture.minFilter = THREE.LinearFilter; return Promise.resolve(texture); }, }; }()); export default COGParser;