Merge pull request #14063 from tschaub/mask-support
GeoTIFF mask support
This commit is contained in:
@@ -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<Array<GeoTIFFImage>>}
|
||||
* @private
|
||||
*/
|
||||
this.sourceMasks_ = new Array(numSources);
|
||||
|
||||
/**
|
||||
* @type {Array<number>}
|
||||
* @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,18 +747,55 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(requests)
|
||||
.then(this.composeTile_.bind(this, sourceTileSize))
|
||||
.catch(function (error) {
|
||||
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;
|
||||
const normalize = this.normalize_;
|
||||
const metadata = this.metadata_;
|
||||
|
||||
return Promise.all(requests)
|
||||
.then(function (sourceSamples) {
|
||||
/** @type {Uint8Array|Float32Array} */
|
||||
let data;
|
||||
if (normalize) {
|
||||
@@ -795,6 +874,13 @@ class GeoTIFFSource extends DataTile {
|
||||
}
|
||||
dataIndex++;
|
||||
}
|
||||
if (!transparent) {
|
||||
const maskIndex = sourceCount + sourceIndex;
|
||||
const mask = sourceSamples[maskIndex];
|
||||
if (mask && !mask[0][pixelIndex]) {
|
||||
transparent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addAlpha) {
|
||||
if (!transparent) {
|
||||
@@ -805,12 +891,6 @@ class GeoTIFFSource extends DataTile {
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(function (error) {
|
||||
// output then rethrow
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
test/rendering/cases/cog-masked/expected.png
Normal file
BIN
test/rendering/cases/cog-masked/expected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
22
test/rendering/cases/cog-masked/main.js
Normal file
22
test/rendering/cases/cog-masked/main.js
Normal file
@@ -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',
|
||||
});
|
||||
BIN
test/rendering/data/raster/masked.tif
Normal file
BIN
test/rendering/data/raster/masked.tif
Normal file
Binary file not shown.
Reference in New Issue
Block a user