diff --git a/src/ol/source/GeoTIFF.js b/src/ol/source/GeoTIFF.js index 24e8a38e34..5888d52075 100644 --- a/src/ol/source/GeoTIFF.js +++ b/src/ol/source/GeoTIFF.js @@ -19,6 +19,18 @@ import {clamp} from '../math.js'; import {getCenter, getIntersection} from '../extent.js'; import {fromCode as unitsFromCode} from '../proj/Units.js'; +/** + * Determine if an image type is a mask. + * See https://www.awaresystems.be/imaging/tiff/tifftags/newsubfiletype.html + * @param {GeoTIFFImage} image The image. + * @return {boolean} The image is a mask. + */ +function isMask(image) { + const fileDirectory = image.fileDirectory; + const type = fileDirectory.NewSubfileType || 0; + return (type & 4) === 4; +} + /** * @typedef {Object} SourceInfo * @property {string} [url] URL for the source GeoTIFF. @@ -363,6 +375,12 @@ class GeoTIFFSource extends DataTile { */ this.sourceImagery_ = new Array(numSources); + /** + * @type {Array>} + * @private + */ + this.sourceMasks_ = new Array(numSources); + /** * @type {Array} * @private @@ -467,8 +485,22 @@ class GeoTIFFSource extends DataTile { const sourceCount = sources.length; for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { - const images = sources[sourceIndex]; + const images = []; + const masks = []; + sources[sourceIndex].forEach((item) => { + if (isMask(item)) { + masks.push(item); + } else { + images.push(item); + } + }); + const imageCount = images.length; + if (masks.length > 0 && masks.length !== imageCount) { + throw new Error( + `Expected one mask per image found ${masks.length} masks and ${imageCount} images` + ); + } let sourceExtent; let sourceOrigin; @@ -574,6 +606,7 @@ class GeoTIFFSource extends DataTile { } this.sourceImagery_[sourceIndex] = images.reverse(); + this.sourceMasks_[sourceIndex] = masks.reverse(); } for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) { @@ -606,6 +639,10 @@ class GeoTIFFSource extends DataTile { this.addAlpha_ = true; break; } + if (this.sourceMasks_[sourceIndex].length) { + this.addAlpha_ = true; + break; + } const values = nodataValues[sourceIndex]; @@ -659,15 +696,20 @@ class GeoTIFFSource extends DataTile { }); } + /** + * @param {number} z The z tile index. + * @param {number} x The x tile index. + * @param {number} y The y tile index. + * @return {Promise} The composed tile data. + * @private + */ loadTile_(z, x, y) { const sourceTileSize = this.getTileSize(z); const sourceCount = this.sourceImagery_.length; - const requests = new Array(sourceCount); - const addAlpha = this.addAlpha_; - const bandCount = this.bandCount; - const samplesPerPixel = this.samplesPerPixel_; + const requests = new Array(sourceCount * 2); const nodataValues = this.nodataValues_; const sourceInfo = this.sourceInfo_; + const pool = getWorkerPool(); for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { const source = sourceInfo[sourceIndex]; const resolutionFactor = this.resolutionFactors_[sourceIndex]; @@ -705,113 +747,151 @@ class GeoTIFFSource extends DataTile { height: sourceTileSize[1], samples: samples, fillValue: fillValue, - pool: getWorkerPool(), + pool: pool, + interleave: false, + }); + + // requests after `sourceCount` are for mask data (if any) + const maskIndex = sourceCount + sourceIndex; + const mask = this.sourceMasks_[sourceIndex][z]; + if (!mask) { + requests[maskIndex] = Promise.resolve(null); + continue; + } + + requests[maskIndex] = mask.readRasters({ + window: pixelBounds, + width: sourceTileSize[0], + height: sourceTileSize[1], + samples: [0], + pool: pool, interleave: false, }); } - const pixelCount = sourceTileSize[0] * sourceTileSize[1]; - const dataLength = pixelCount * bandCount; - const normalize = this.normalize_; - const metadata = this.metadata_; - return Promise.all(requests) - .then(function (sourceSamples) { - /** @type {Uint8Array|Float32Array} */ - let data; - if (normalize) { - data = new Uint8Array(dataLength); - } else { - data = new Float32Array(dataLength); - } - - let dataIndex = 0; - for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) { - let transparent = addAlpha; - for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { - const source = sourceInfo[sourceIndex]; - - let min = source.min; - let max = source.max; - let gain, bias; - if (normalize) { - const stats = metadata[sourceIndex][0]; - if (min === undefined) { - if (stats && STATISTICS_MINIMUM in stats) { - min = parseFloat(stats[STATISTICS_MINIMUM]); - } else { - min = getMinForDataType(sourceSamples[sourceIndex][0]); - } - } - if (max === undefined) { - if (stats && STATISTICS_MAXIMUM in stats) { - max = parseFloat(stats[STATISTICS_MAXIMUM]); - } else { - max = getMaxForDataType(sourceSamples[sourceIndex][0]); - } - } - - gain = 255 / (max - min); - bias = -min * gain; - } - - for ( - let sampleIndex = 0; - sampleIndex < samplesPerPixel[sourceIndex]; - ++sampleIndex - ) { - const sourceValue = - sourceSamples[sourceIndex][sampleIndex][pixelIndex]; - - let value; - if (normalize) { - value = clamp(gain * sourceValue + bias, 0, 255); - } else { - value = sourceValue; - } - - if (!addAlpha) { - data[dataIndex] = value; - } else { - let nodata = source.nodata; - if (nodata === undefined) { - let bandIndex; - if (source.bands) { - bandIndex = source.bands[sampleIndex] - 1; - } else { - bandIndex = sampleIndex; - } - nodata = nodataValues[sourceIndex][bandIndex]; - } - - const nodataIsNaN = isNaN(nodata); - if ( - (!nodataIsNaN && sourceValue !== nodata) || - (nodataIsNaN && !isNaN(sourceValue)) - ) { - transparent = false; - data[dataIndex] = value; - } - } - dataIndex++; - } - } - if (addAlpha) { - if (!transparent) { - data[dataIndex] = 255; - } - dataIndex++; - } - } - - return data; - }) + .then(this.composeTile_.bind(this, sourceTileSize)) .catch(function (error) { - // output then rethrow console.error(error); // eslint-disable-line no-console throw error; }); } + + /** + * @param {import("../size.js").Size} sourceTileSize The source tile size. + * @param {Array} sourceSamples The source samples. + * @return {import("../DataTile.js").Data} The composed tile data. + * @private + */ + composeTile_(sourceTileSize, sourceSamples) { + const metadata = this.metadata_; + const sourceInfo = this.sourceInfo_; + const sourceCount = this.sourceImagery_.length; + const bandCount = this.bandCount; + const samplesPerPixel = this.samplesPerPixel_; + const nodataValues = this.nodataValues_; + const normalize = this.normalize_; + const addAlpha = this.addAlpha_; + + const pixelCount = sourceTileSize[0] * sourceTileSize[1]; + const dataLength = pixelCount * bandCount; + + /** @type {Uint8Array|Float32Array} */ + let data; + if (normalize) { + data = new Uint8Array(dataLength); + } else { + data = new Float32Array(dataLength); + } + + let dataIndex = 0; + for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) { + let transparent = addAlpha; + for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { + const source = sourceInfo[sourceIndex]; + + let min = source.min; + let max = source.max; + let gain, bias; + if (normalize) { + const stats = metadata[sourceIndex][0]; + if (min === undefined) { + if (stats && STATISTICS_MINIMUM in stats) { + min = parseFloat(stats[STATISTICS_MINIMUM]); + } else { + min = getMinForDataType(sourceSamples[sourceIndex][0]); + } + } + if (max === undefined) { + if (stats && STATISTICS_MAXIMUM in stats) { + max = parseFloat(stats[STATISTICS_MAXIMUM]); + } else { + max = getMaxForDataType(sourceSamples[sourceIndex][0]); + } + } + + gain = 255 / (max - min); + bias = -min * gain; + } + + for ( + let sampleIndex = 0; + sampleIndex < samplesPerPixel[sourceIndex]; + ++sampleIndex + ) { + const sourceValue = + sourceSamples[sourceIndex][sampleIndex][pixelIndex]; + + let value; + if (normalize) { + value = clamp(gain * sourceValue + bias, 0, 255); + } else { + value = sourceValue; + } + + if (!addAlpha) { + data[dataIndex] = value; + } else { + let nodata = source.nodata; + if (nodata === undefined) { + let bandIndex; + if (source.bands) { + bandIndex = source.bands[sampleIndex] - 1; + } else { + bandIndex = sampleIndex; + } + nodata = nodataValues[sourceIndex][bandIndex]; + } + + const nodataIsNaN = isNaN(nodata); + if ( + (!nodataIsNaN && sourceValue !== nodata) || + (nodataIsNaN && !isNaN(sourceValue)) + ) { + transparent = false; + data[dataIndex] = value; + } + } + dataIndex++; + } + if (!transparent) { + const maskIndex = sourceCount + sourceIndex; + const mask = sourceSamples[maskIndex]; + if (mask && !mask[0][pixelIndex]) { + transparent = true; + } + } + } + if (addAlpha) { + if (!transparent) { + data[dataIndex] = 255; + } + dataIndex++; + } + } + + return data; + } } /** diff --git a/test/rendering/cases/cog-masked/expected.png b/test/rendering/cases/cog-masked/expected.png new file mode 100644 index 0000000000..8a5ec720c1 Binary files /dev/null and b/test/rendering/cases/cog-masked/expected.png differ diff --git a/test/rendering/cases/cog-masked/main.js b/test/rendering/cases/cog-masked/main.js new file mode 100644 index 0000000000..1aefa5b953 --- /dev/null +++ b/test/rendering/cases/cog-masked/main.js @@ -0,0 +1,22 @@ +import GeoTIFF from '../../../../src/ol/source/GeoTIFF.js'; +import Map from '../../../../src/ol/Map.js'; +import TileLayer from '../../../../src/ol/layer/WebGLTile.js'; + +const source = new GeoTIFF({ + convertToRGB: true, + sources: [{url: '/data/raster/masked.tif'}], +}); + +new Map({ + layers: [ + new TileLayer({ + source: source, + }), + ], + target: 'map', + view: source.getView(), +}); + +render({ + message: 'works with geotiffs that include a mask', +}); diff --git a/test/rendering/data/raster/masked.tif b/test/rendering/data/raster/masked.tif new file mode 100644 index 0000000000..d34fbf612f Binary files /dev/null and b/test/rendering/data/raster/masked.tif differ