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,18 +747,55 @@ 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 pixelCount = sourceTileSize[0] * sourceTileSize[1];
|
||||||
const dataLength = pixelCount * bandCount;
|
const dataLength = pixelCount * bandCount;
|
||||||
const normalize = this.normalize_;
|
|
||||||
const metadata = this.metadata_;
|
|
||||||
|
|
||||||
return Promise.all(requests)
|
|
||||||
.then(function (sourceSamples) {
|
|
||||||
/** @type {Uint8Array|Float32Array} */
|
/** @type {Uint8Array|Float32Array} */
|
||||||
let data;
|
let data;
|
||||||
if (normalize) {
|
if (normalize) {
|
||||||
@@ -795,6 +874,13 @@ class GeoTIFFSource extends DataTile {
|
|||||||
}
|
}
|
||||||
dataIndex++;
|
dataIndex++;
|
||||||
}
|
}
|
||||||
|
if (!transparent) {
|
||||||
|
const maskIndex = sourceCount + sourceIndex;
|
||||||
|
const mask = sourceSamples[maskIndex];
|
||||||
|
if (mask && !mask[0][pixelIndex]) {
|
||||||
|
transparent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (addAlpha) {
|
if (addAlpha) {
|
||||||
if (!transparent) {
|
if (!transparent) {
|
||||||
@@ -805,12 +891,6 @@ class GeoTIFFSource extends DataTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
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