Mix Geotiffs with arbitrary bands and resolutions
This commit is contained in:
@@ -244,6 +244,7 @@ function parseStyle(style, bandCount) {
|
|||||||
* property on the layer object; for example, setting `title: 'My Title'` in the
|
* property on the layer object; for example, setting `title: 'My Title'` in the
|
||||||
* options means that `title` is observable, and has get/set accessors.
|
* options means that `title` is observable, and has get/set accessors.
|
||||||
*
|
*
|
||||||
|
* @extends BaseTileLayer<import("../source/DataTile.js").default|import("../source/TileImage.js").default>
|
||||||
* @api
|
* @api
|
||||||
*/
|
*/
|
||||||
class WebGLTileLayer extends BaseTileLayer {
|
class WebGLTileLayer extends BaseTileLayer {
|
||||||
@@ -257,12 +258,10 @@ class WebGLTileLayer extends BaseTileLayer {
|
|||||||
delete options.style;
|
delete options.style;
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
const parsedStyle = parseStyle(style || {}, 1); // TODO: get texture count from source
|
/**
|
||||||
|
* @type {Style}
|
||||||
this.vertexShader_ = parsedStyle.vertexShader;
|
*/
|
||||||
this.fragmentShader_ = parsedStyle.fragmentShader;
|
this.style_ = style;
|
||||||
this.uniforms_ = parsedStyle.uniforms;
|
|
||||||
this.styleVariables_ = style.variables || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,10 +270,18 @@ class WebGLTileLayer extends BaseTileLayer {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
createRenderer() {
|
createRenderer() {
|
||||||
|
const source = this.getSource();
|
||||||
|
const parsedStyle = parseStyle(
|
||||||
|
this.style_,
|
||||||
|
'bandCount' in source ? source.bandCount : 4
|
||||||
|
);
|
||||||
|
|
||||||
|
this.styleVariables_ = this.style_.variables || {};
|
||||||
|
|
||||||
return new WebGLTileLayerRenderer(this, {
|
return new WebGLTileLayerRenderer(this, {
|
||||||
vertexShader: this.vertexShader_,
|
vertexShader: parsedStyle.vertexShader,
|
||||||
fragmentShader: this.fragmentShader_,
|
fragmentShader: parsedStyle.fragmentShader,
|
||||||
uniforms: this.uniforms_,
|
uniforms: parsedStyle.uniforms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ class DataTileSource extends TileSource {
|
|||||||
this.loader_ = options.loader;
|
this.loader_ = options.loader;
|
||||||
|
|
||||||
this.handleTileChange_ = this.handleTileChange_.bind(this);
|
this.handleTileChange_ = this.handleTileChange_.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.bandCount = 4; // assume RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import State from './State.js';
|
|||||||
import TileGrid from '../tilegrid/TileGrid.js';
|
import TileGrid from '../tilegrid/TileGrid.js';
|
||||||
import {Pool, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff';
|
import {Pool, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff';
|
||||||
import {create as createDecoderWorker} from '../worker/geotiff-decoder.js';
|
import {create as createDecoderWorker} from '../worker/geotiff-decoder.js';
|
||||||
|
import {getIntersection} from '../extent.js';
|
||||||
import {get as getProjection} from '../proj.js';
|
import {get as getProjection} from '../proj.js';
|
||||||
import {toSize} from '../size.js';
|
import {toSize} from '../size.js';
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ import {toSize} from '../size.js';
|
|||||||
* @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on
|
* @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on
|
||||||
* the configured min and max.
|
* the configured min and max.
|
||||||
* @property {number} [nodata] Values to discard.
|
* @property {number} [nodata] Values to discard.
|
||||||
|
* @property {Array<number>} [samples] Indices of the samples to be read from. If not provided, all samples
|
||||||
|
* will be read.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let workerPool;
|
let workerPool;
|
||||||
@@ -59,21 +62,23 @@ function getImagesForSource(source) {
|
|||||||
/**
|
/**
|
||||||
* @param {number|Array<number>|Array<Array<number>>} expected Expected value.
|
* @param {number|Array<number>|Array<Array<number>>} expected Expected value.
|
||||||
* @param {number|Array<number>|Array<Array<number>>} got Actual value.
|
* @param {number|Array<number>|Array<Array<number>>} got Actual value.
|
||||||
|
* @param {number} tolerance Accepted tolerance in fraction of expected between expected and got.
|
||||||
* @param {string} message The error message.
|
* @param {string} message The error message.
|
||||||
*/
|
*/
|
||||||
function assertEqual(expected, got, message) {
|
function assertEqual(expected, got, tolerance, message) {
|
||||||
if (Array.isArray(expected)) {
|
if (Array.isArray(expected)) {
|
||||||
const length = expected.length;
|
const length = expected.length;
|
||||||
if (!Array.isArray(got) || length != got.length) {
|
if (!Array.isArray(got) || length != got.length) {
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < length; ++i) {
|
for (let i = 0; i < length; ++i) {
|
||||||
assertEqual(expected[i], got[i], message);
|
assertEqual(expected[i], got[i], tolerance, message);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expected !== got) {
|
got = /** @type {number} */ (got);
|
||||||
|
if (Math.abs(expected - got) > tolerance * expected) {
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +138,9 @@ function getMaxForDataType(array) {
|
|||||||
/**
|
/**
|
||||||
* @typedef Options
|
* @typedef Options
|
||||||
* @property {Array<SourceInfo>} sources List of information about GeoTIFF sources.
|
* @property {Array<SourceInfo>} sources List of information about GeoTIFF sources.
|
||||||
|
* When using multiple sources, each source must be a single-band source, or the `samples`
|
||||||
|
* option must be configured with a single sample index for each source. Multiple sources
|
||||||
|
* can only be combined when their resolution sets are equal after applying a scale.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,12 +172,24 @@ class GeoTIFFSource extends DataTile {
|
|||||||
*/
|
*/
|
||||||
this.sourceImagery_ = new Array(numSources);
|
this.sourceImagery_ = new Array(numSources);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<number>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.resolutionFactors_ = new Array(numSources);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.samplesPerPixel_;
|
this.samplesPerPixel_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.addAlpha_ = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Error}
|
* @type {Error}
|
||||||
* @private
|
* @private
|
||||||
@@ -192,7 +212,16 @@ class GeoTIFFSource extends DataTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Error} A source loading error.
|
* @return {Error} A source loading error. When the source state is `error`, use this function
|
||||||
|
* to get more information about the error. To debug a faulty configuration, you may want to use
|
||||||
|
* a listener like
|
||||||
|
* ```js
|
||||||
|
* geotiffSource.on('change', () => {
|
||||||
|
* if (geotiffSource.getState() === 'error') {
|
||||||
|
* console.error(geotiffSource.getError());
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
getError() {
|
getError() {
|
||||||
return this.error_;
|
return this.error_;
|
||||||
@@ -203,6 +232,7 @@ class GeoTIFFSource extends DataTile {
|
|||||||
* must have the same internal tiled structure.
|
* must have the same internal tiled structure.
|
||||||
* @param {Array<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} sources Each source is a list of images
|
* @param {Array<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} sources Each source is a list of images
|
||||||
* from a single GeoTIFF.
|
* from a single GeoTIFF.
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
configure_(sources) {
|
configure_(sources) {
|
||||||
let extent;
|
let extent;
|
||||||
@@ -210,6 +240,7 @@ class GeoTIFFSource extends DataTile {
|
|||||||
let tileSizes;
|
let tileSizes;
|
||||||
let resolutions;
|
let resolutions;
|
||||||
let samplesPerPixel;
|
let samplesPerPixel;
|
||||||
|
let minZoom = 0;
|
||||||
|
|
||||||
const sourceCount = sources.length;
|
const sourceCount = sources.length;
|
||||||
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
||||||
@@ -223,12 +254,15 @@ class GeoTIFFSource extends DataTile {
|
|||||||
|
|
||||||
for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) {
|
for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) {
|
||||||
const image = images[imageIndex];
|
const image = images[imageIndex];
|
||||||
const imageSamplesPerPixel = image.getSamplesPerPixel();
|
const wantedSamples = this.sourceInfo_[sourceIndex].samples;
|
||||||
|
const imageSamplesPerPixel = wantedSamples
|
||||||
|
? wantedSamples.length
|
||||||
|
: image.getSamplesPerPixel();
|
||||||
if (!samplesPerPixel) {
|
if (!samplesPerPixel) {
|
||||||
samplesPerPixel = imageSamplesPerPixel;
|
samplesPerPixel = imageSamplesPerPixel;
|
||||||
} else {
|
} else {
|
||||||
const message = `Band count mismatch for source ${sourceIndex}, got ${imageSamplesPerPixel} but expected ${samplesPerPixel}`;
|
const message = `Band count mismatch for source ${sourceIndex}, got ${imageSamplesPerPixel} but expected ${samplesPerPixel}`;
|
||||||
assertEqual(samplesPerPixel, imageSamplesPerPixel, message);
|
assertEqual(samplesPerPixel, imageSamplesPerPixel, 0, message);
|
||||||
}
|
}
|
||||||
const level = imageCount - (imageIndex + 1);
|
const level = imageCount - (imageIndex + 1);
|
||||||
|
|
||||||
@@ -247,37 +281,60 @@ class GeoTIFFSource extends DataTile {
|
|||||||
if (!extent) {
|
if (!extent) {
|
||||||
extent = sourceExtent;
|
extent = sourceExtent;
|
||||||
} else {
|
} else {
|
||||||
const message = `Extent mismatch for source ${sourceIndex}, got [${sourceExtent}] but expected [${extent}]`;
|
getIntersection(extent, sourceExtent);
|
||||||
assertEqual(extent, sourceExtent, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
origin = sourceOrigin;
|
origin = sourceOrigin;
|
||||||
} else {
|
} else {
|
||||||
const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`;
|
const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`;
|
||||||
assertEqual(origin, sourceOrigin, message);
|
assertEqual(origin, sourceOrigin, 0, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolutions) {
|
||||||
|
resolutions = sourceResolutions;
|
||||||
|
this.resolutionFactors_[sourceIndex] = 1;
|
||||||
|
} else {
|
||||||
|
if (resolutions.length - minZoom > sourceResolutions.length) {
|
||||||
|
minZoom = resolutions.length - sourceResolutions.length;
|
||||||
|
}
|
||||||
|
const resolutionFactor =
|
||||||
|
resolutions[resolutions.length - 1] /
|
||||||
|
sourceResolutions[sourceResolutions.length - 1];
|
||||||
|
this.resolutionFactors_[sourceIndex] = resolutionFactor;
|
||||||
|
const scaledSourceResolutions = sourceResolutions.map(
|
||||||
|
(resolution) => (resolution *= resolutionFactor)
|
||||||
|
);
|
||||||
|
const message = `Resolution mismatch for source ${sourceIndex}, got [${scaledSourceResolutions}] but expected [${resolutions}]`;
|
||||||
|
assertEqual(
|
||||||
|
resolutions.slice(minZoom, resolutions.length),
|
||||||
|
scaledSourceResolutions,
|
||||||
|
0.005,
|
||||||
|
message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tileSizes) {
|
if (!tileSizes) {
|
||||||
tileSizes = sourceTileSizes;
|
tileSizes = sourceTileSizes;
|
||||||
} else {
|
} else {
|
||||||
assertEqual(
|
assertEqual(
|
||||||
tileSizes,
|
tileSizes.slice(minZoom, tileSizes.length),
|
||||||
sourceTileSizes,
|
sourceTileSizes,
|
||||||
|
0,
|
||||||
`Tile size mismatch for source ${sourceIndex}`
|
`Tile size mismatch for source ${sourceIndex}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolutions) {
|
|
||||||
resolutions = sourceResolutions;
|
|
||||||
} else {
|
|
||||||
const message = `Resolution mismatch for source ${sourceIndex}, got [${sourceResolutions}] but expected [${resolutions}]`;
|
|
||||||
assertEqual(resolutions, sourceResolutions, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sourceImagery_[sourceIndex] = images.reverse();
|
this.sourceImagery_[sourceIndex] = images.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) {
|
||||||
|
const sourceImagery = this.sourceImagery_[i];
|
||||||
|
while (sourceImagery.length < resolutions.length) {
|
||||||
|
sourceImagery.unshift(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.getProjection()) {
|
if (!this.getProjection()) {
|
||||||
const firstImage = sources[0][0];
|
const firstImage = sources[0][0];
|
||||||
if (firstImage.geoKeys) {
|
if (firstImage.geoKeys) {
|
||||||
@@ -297,9 +354,26 @@ class GeoTIFFSource extends DataTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.samplesPerPixel_ = samplesPerPixel;
|
this.samplesPerPixel_ = samplesPerPixel;
|
||||||
|
const sourceInfo = this.sourceInfo_;
|
||||||
|
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
||||||
|
if (sourceInfo[sourceIndex].nodata !== undefined) {
|
||||||
|
this.addAlpha_ = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let additionalBands = 0;
|
||||||
|
if (this.addAlpha_) {
|
||||||
|
if (sourceCount === 2 && samplesPerPixel === 1) {
|
||||||
|
additionalBands = 2;
|
||||||
|
} else {
|
||||||
|
additionalBands = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.bandCount = samplesPerPixel * sourceCount + additionalBands;
|
||||||
|
|
||||||
const tileGrid = new TileGrid({
|
const tileGrid = new TileGrid({
|
||||||
extent: extent,
|
extent: extent,
|
||||||
|
minZoom: minZoom,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
resolutions: resolutions,
|
resolutions: resolutions,
|
||||||
tileSizes: tileSizes,
|
tileSizes: tileSizes,
|
||||||
@@ -313,38 +387,32 @@ class GeoTIFFSource extends DataTile {
|
|||||||
|
|
||||||
loadTile_(z, x, y) {
|
loadTile_(z, x, y) {
|
||||||
const size = toSize(this.tileGrid.getTileSize(z));
|
const size = toSize(this.tileGrid.getTileSize(z));
|
||||||
const pixelBounds = [
|
|
||||||
x * size[0],
|
|
||||||
y * size[1],
|
|
||||||
(x + 1) * size[0],
|
|
||||||
(y + 1) * size[1],
|
|
||||||
];
|
|
||||||
|
|
||||||
const sourceCount = this.sourceImagery_.length;
|
const sourceCount = this.sourceImagery_.length;
|
||||||
const requests = new Array(sourceCount);
|
const requests = new Array(sourceCount);
|
||||||
let addAlpha = false;
|
const addAlpha = this.addAlpha_;
|
||||||
|
const bandCount = this.bandCount;
|
||||||
|
const samplesPerPixel = this.samplesPerPixel_;
|
||||||
const sourceInfo = this.sourceInfo_;
|
const sourceInfo = this.sourceInfo_;
|
||||||
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
|
||||||
|
const source = sourceInfo[sourceIndex];
|
||||||
|
const resolutionFactor = this.resolutionFactors_[sourceIndex];
|
||||||
|
const pixelBounds = [
|
||||||
|
Math.round(x * (size[0] * resolutionFactor)),
|
||||||
|
Math.round(y * (size[1] * resolutionFactor)),
|
||||||
|
Math.round((x + 1) * (size[0] * resolutionFactor)),
|
||||||
|
Math.round((y + 1) * (size[1] * resolutionFactor)),
|
||||||
|
];
|
||||||
const image = this.sourceImagery_[sourceIndex][z];
|
const image = this.sourceImagery_[sourceIndex][z];
|
||||||
requests[sourceIndex] = image.readRasters({
|
requests[sourceIndex] = image.readRasters({
|
||||||
window: pixelBounds,
|
window: pixelBounds,
|
||||||
|
width: size[0],
|
||||||
|
height: size[1],
|
||||||
|
samples: source.samples,
|
||||||
pool: getWorkerPool(),
|
pool: getWorkerPool(),
|
||||||
});
|
});
|
||||||
if (sourceInfo[sourceIndex].nodata !== undefined) {
|
|
||||||
addAlpha = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const samplesPerPixel = this.samplesPerPixel_;
|
|
||||||
let additionalBands = 0;
|
|
||||||
if (addAlpha) {
|
|
||||||
if (sourceCount === 2 && samplesPerPixel === 1) {
|
|
||||||
additionalBands = 2;
|
|
||||||
} else {
|
|
||||||
additionalBands = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const bandCount = samplesPerPixel * sourceCount + additionalBands;
|
|
||||||
const pixelCount = size[0] * size[1];
|
const pixelCount = size[0] * size[1];
|
||||||
const dataLength = pixelCount * bandCount;
|
const dataLength = pixelCount * bandCount;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user