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 {getCenter, getIntersection} from '../extent.js';
|
||||||
import {fromCode as unitsFromCode} from '../proj/Units.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
|
* @typedef {Object} SourceInfo
|
||||||
* @property {string} [url] URL for the source GeoTIFF.
|
* @property {string} [url] URL for the source GeoTIFF.
|
||||||
@@ -363,6 +375,12 @@ class GeoTIFFSource extends DataTile {
|
|||||||
*/
|
*/
|
||||||
this.sourceImagery_ = new Array(numSources);
|
this.sourceImagery_ = new Array(numSources);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<Array<GeoTIFFImage>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.sourceMasks_ = new Array(numSources);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Array<number>}
|
* @type {Array<number>}
|
||||||
* @private
|
* @private
|
||||||
@@ -467,8 +485,22 @@ class GeoTIFFSource extends DataTile {
|
|||||||
|
|
||||||
const sourceCount = sources.length;
|
const sourceCount = sources.length;
|
||||||
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
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;
|
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 sourceExtent;
|
||||||
let sourceOrigin;
|
let sourceOrigin;
|
||||||
@@ -574,6 +606,7 @@ class GeoTIFFSource extends DataTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sourceImagery_[sourceIndex] = images.reverse();
|
this.sourceImagery_[sourceIndex] = images.reverse();
|
||||||
|
this.sourceMasks_[sourceIndex] = masks.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) {
|
for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) {
|
||||||
@@ -606,6 +639,10 @@ class GeoTIFFSource extends DataTile {
|
|||||||
this.addAlpha_ = true;
|
this.addAlpha_ = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (this.sourceMasks_[sourceIndex].length) {
|
||||||
|
this.addAlpha_ = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const values = nodataValues[sourceIndex];
|
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) {
|
loadTile_(z, x, y) {
|
||||||
const sourceTileSize = this.getTileSize(z);
|
const sourceTileSize = this.getTileSize(z);
|
||||||
const sourceCount = this.sourceImagery_.length;
|
const sourceCount = this.sourceImagery_.length;
|
||||||
const requests = new Array(sourceCount);
|
const requests = new Array(sourceCount * 2);
|
||||||
const addAlpha = this.addAlpha_;
|
|
||||||
const bandCount = this.bandCount;
|
|
||||||
const samplesPerPixel = this.samplesPerPixel_;
|
|
||||||
const nodataValues = this.nodataValues_;
|
const nodataValues = this.nodataValues_;
|
||||||
const sourceInfo = this.sourceInfo_;
|
const sourceInfo = this.sourceInfo_;
|
||||||
|
const pool = getWorkerPool();
|
||||||
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
||||||
const source = sourceInfo[sourceIndex];
|
const source = sourceInfo[sourceIndex];
|
||||||
const resolutionFactor = this.resolutionFactors_[sourceIndex];
|
const resolutionFactor = this.resolutionFactors_[sourceIndex];
|
||||||
@@ -705,113 +747,151 @@ class GeoTIFFSource extends DataTile {
|
|||||||
height: sourceTileSize[1],
|
height: sourceTileSize[1],
|
||||||
samples: samples,
|
samples: samples,
|
||||||
fillValue: fillValue,
|
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,
|
interleave: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pixelCount = sourceTileSize[0] * sourceTileSize[1];
|
|
||||||
const dataLength = pixelCount * bandCount;
|
|
||||||
const normalize = this.normalize_;
|
|
||||||
const metadata = this.metadata_;
|
|
||||||
|
|
||||||
return Promise.all(requests)
|
return Promise.all(requests)
|
||||||
.then(function (sourceSamples) {
|
.then(this.composeTile_.bind(this, sourceTileSize))
|
||||||
/** @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;
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
// output then rethrow
|
|
||||||
console.error(error); // eslint-disable-line no-console
|
console.error(error); // eslint-disable-line no-console
|
||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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