/** * @module ol/source/GeoTIFF */ import DataTile from './DataTile.js'; import TileGrid from '../tilegrid/TileGrid.js'; import { Pool, globals as geotiffGlobals, fromBlob as tiffFromBlob, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls, } from 'geotiff'; import { Projection, get as getCachedProjection, toUserCoordinate, toUserExtent, } from '../proj.js'; import {clamp} from '../math.js'; import {getCenter, getIntersection} from '../extent.js'; import {error as logError} from '../console.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; } /** * @param {true|false|'auto'} preference The convertToRGB option. * @param {GeoTIFFImage} image The image. * @return {boolean} Use the `image.readRGB()` method. */ function readRGB(preference, image) { if (!preference) { return false; } if (preference === true) { return true; } if (image.getSamplesPerPixel() !== 3) { return false; } const interpretation = image.fileDirectory.PhotometricInterpretation; const interpretations = geotiffGlobals.photometricInterpretations; return ( interpretation === interpretations.CMYK || interpretation === interpretations.YCbCr || interpretation === interpretations.CIELab || interpretation === interpretations.ICCLab ); } /** * @typedef {Object} SourceInfo * @property {string} [url] URL for the source GeoTIFF. * @property {Array} [overviews] List of any overview URLs, only applies if the url parameter is given. * @property {Blob} [blob] Blob containing the source GeoTIFF. `blob` and `url` are mutually exclusive. * @property {number} [min=0] The minimum source data value. Rendered values are scaled from 0 to 1 based on * the configured min and max. If not provided and raster statistics are available, those will be used instead. * If neither are available, the minimum for the data type will be used. To disable this behavior, set * the `normalize` option to `false` in the constructor. * @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on * the configured min and max. If not provided and raster statistics are available, those will be used instead. * If neither are available, the maximum for the data type will be used. To disable this behavior, set * the `normalize` option to `false` in the constructor. * @property {number} [nodata] Values to discard (overriding any nodata values in the metadata). * When provided, an additional alpha band will be added to the data. Often the GeoTIFF metadata * will include information about nodata values, so you should only need to set this property if * you find that it is not already extracted from the metadata. * @property {Array} [bands] Band numbers to be read from (where the first band is `1`). If not provided, all bands will * be read. For example, if a GeoTIFF has blue (1), green (2), red (3), and near-infrared (4) bands, and you only need the * near-infrared band, configure `bands: [4]`. */ /** * @typedef {Object} GeoKeys * @property {number} GTModelTypeGeoKey Model type. * @property {number} GTRasterTypeGeoKey Raster type. * @property {number} GeogAngularUnitsGeoKey Angular units. * @property {number} GeogInvFlatteningGeoKey Inverse flattening. * @property {number} GeogSemiMajorAxisGeoKey Semi-major axis. * @property {number} GeographicTypeGeoKey Geographic coordinate system code. * @property {number} ProjLinearUnitsGeoKey Projected linear unit code. * @property {number} ProjectedCSTypeGeoKey Projected coordinate system code. */ /** * @typedef {import("geotiff").GeoTIFF} GeoTIFF */ /** * @typedef {import("geotiff").MultiGeoTIFF} MultiGeoTIFF */ /** * @typedef {Object} GDALMetadata * @property {string} STATISTICS_MINIMUM The minimum value (as a string). * @property {string} STATISTICS_MAXIMUM The maximum value (as a string). */ const STATISTICS_MAXIMUM = 'STATISTICS_MAXIMUM'; const STATISTICS_MINIMUM = 'STATISTICS_MINIMUM'; const defaultTileSize = 256; /** * @typedef {import("geotiff").GeoTIFFImage} GeoTIFFImage */ let workerPool; function getWorkerPool() { if (!workerPool) { workerPool = new Pool(); } return workerPool; } /** * Get the bounding box of an image. If the image does not have an affine transform, * the pixel bounds are returned. * @param {GeoTIFFImage} image The image. * @return {Array} The image bounding box. */ function getBoundingBox(image) { try { return image.getBoundingBox(); } catch (_) { return [0, 0, image.getWidth(), image.getHeight()]; } } /** * Get the origin of an image. If the image does not have an affine transform, * the top-left corner of the pixel bounds is returned. * @param {GeoTIFFImage} image The image. * @return {Array} The image origin. */ function getOrigin(image) { try { return image.getOrigin().slice(0, 2); } catch (_) { return [0, image.getHeight()]; } } /** * Get the resolution of an image. If the image does not have an affine transform, * the width of the image is compared with the reference image. * @param {GeoTIFFImage} image The image. * @param {GeoTIFFImage} referenceImage The reference image. * @return {Array} The map x and y units per pixel. */ function getResolutions(image, referenceImage) { try { return image.getResolution(referenceImage); } catch (_) { return [ referenceImage.getWidth() / image.getWidth(), referenceImage.getHeight() / image.getHeight(), ]; } } /** * @param {GeoTIFFImage} image A GeoTIFF. * @return {import("../proj/Projection.js").default} The image projection. */ function getProjection(image) { const geoKeys = image.geoKeys; if (!geoKeys) { return null; } if ( geoKeys.ProjectedCSTypeGeoKey && geoKeys.ProjectedCSTypeGeoKey !== 32767 ) { const code = 'EPSG:' + geoKeys.ProjectedCSTypeGeoKey; let projection = getCachedProjection(code); if (!projection) { const units = unitsFromCode(geoKeys.ProjLinearUnitsGeoKey); if (units) { projection = new Projection({ code: code, units: units, }); } } return projection; } if (geoKeys.GeographicTypeGeoKey && geoKeys.GeographicTypeGeoKey !== 32767) { const code = 'EPSG:' + geoKeys.GeographicTypeGeoKey; let projection = getCachedProjection(code); if (!projection) { const units = unitsFromCode(geoKeys.GeogAngularUnitsGeoKey); if (units) { projection = new Projection({ code: code, units: units, }); } } return projection; } return null; } /** * @param {GeoTIFF|MultiGeoTIFF} tiff A GeoTIFF. * @return {Promise>} Resolves to a list of images. */ function getImagesForTIFF(tiff) { return tiff.getImageCount().then(function (count) { const requests = new Array(count); for (let i = 0; i < count; ++i) { requests[i] = tiff.getImage(i); } return Promise.all(requests); }); } /** * @param {SourceInfo} source The GeoTIFF source. * @param {Object} options Options for the GeoTIFF source. * @return {Promise>} Resolves to a list of images. */ function getImagesForSource(source, options) { let request; if (source.blob) { request = tiffFromBlob(source.blob); } else if (source.overviews) { request = tiffFromUrls(source.url, source.overviews, options); } else { request = tiffFromUrl(source.url, options); } return request.then(getImagesForTIFF); } /** * @param {number|Array|Array>} expected Expected value. * @param {number|Array|Array>} got Actual value. * @param {number} tolerance Accepted tolerance in fraction of expected between expected and got. * @param {string} message The error message. * @param {function(Error):void} rejector A function to be called with any error. */ function assertEqual(expected, got, tolerance, message, rejector) { if (Array.isArray(expected)) { const length = expected.length; if (!Array.isArray(got) || length != got.length) { const error = new Error(message); rejector(error); throw error; } for (let i = 0; i < length; ++i) { assertEqual(expected[i], got[i], tolerance, message, rejector); } return; } got = /** @type {number} */ (got); if (Math.abs(expected - got) > tolerance * expected) { throw new Error(message); } } /** * @param {Array} array The data array. * @return {number} The minimum value. */ function getMinForDataType(array) { if (array instanceof Int8Array) { return -128; } if (array instanceof Int16Array) { return -32768; } if (array instanceof Int32Array) { return -2147483648; } if (array instanceof Float32Array) { return 1.2e-38; } return 0; } /** * @param {Array} array The data array. * @return {number} The maximum value. */ function getMaxForDataType(array) { if (array instanceof Int8Array) { return 127; } if (array instanceof Uint8Array) { return 255; } if (array instanceof Uint8ClampedArray) { return 255; } if (array instanceof Int16Array) { return 32767; } if (array instanceof Uint16Array) { return 65535; } if (array instanceof Int32Array) { return 2147483647; } if (array instanceof Uint32Array) { return 4294967295; } if (array instanceof Float32Array) { return 3.4e38; } return 255; } /** * @typedef {Object} GeoTIFFSourceOptions * @property {boolean} [forceXHR=false] Whether to force the usage of the browsers XMLHttpRequest API. * @property {Object} [headers] additional key-value pairs of headers to be passed with each request. Key is the header name, value the header value. * @property {string} [credentials] How credentials shall be handled. See * https://developer.mozilla.org/en-US/docs/Web/API/fetch for reference and possible values * @property {number} [maxRanges] The maximum amount of ranges to request in a single multi-range request. * By default only a single range is used. * @property {boolean} [allowFullFile=false] Whether or not a full file is accepted when only a portion is * requested. Only use this when you know the source image to be small enough to fit in memory. * @property {number} [blockSize=65536] The block size to use. * @property {number} [cacheSize=100] The number of blocks that shall be held in a LRU cache. */ /** * @typedef {Object} Options * @property {Array} sources List of information about GeoTIFF sources. * Multiple sources can be combined when their resolution sets are equal after applying a scale. * The list of sources defines a mapping between input bands as they are read from each GeoTIFF and * the output bands that are provided by data tiles. To control which bands to read from each GeoTIFF, * use the {@link import("./GeoTIFF.js").SourceInfo bands} property. If, for example, you specify two * sources, one with 3 bands and {@link import("./GeoTIFF.js").SourceInfo nodata} configured, and * another with 1 band, the resulting data tiles will have 5 bands: 3 from the first source, 1 alpha * band from the first source, and 1 band from the second source. * @property {GeoTIFFSourceOptions} [sourceOptions] Additional options to be passed to [geotiff.js](https://geotiffjs.github.io/geotiff.js/module-geotiff.html)'s `fromUrl` or `fromUrls` methods. * @property {true|false|'auto'} [convertToRGB=false] By default, bands from the sources are read as-is. When * reading GeoTIFFs with the purpose of displaying them as RGB images, setting this to `true` will * convert other color spaces (YCbCr, CMYK) to RGB. Setting the option to `'auto'` will make it so CMYK, YCbCr, * CIELab, and ICCLab images will automatically be converted to RGB. * @property {boolean} [normalize=true] By default, the source data is normalized to values between * 0 and 1 with scaling factors based on the raster statistics or `min` and `max` properties of each source. * If instead you want to work with the raw values in a style expression, set this to `false`. Setting this option * to `false` will make it so any `min` and `max` properties on sources are ignored. * @property {boolean} [opaque=false] Whether the layer is opaque. * @property {number} [transition=250] Duration of the opacity transition for rendering. * To disable the opacity transition, pass `transition: 0`. * @property {boolean} [wrapX=false] Render tiles beyond the tile grid extent. * @property {boolean} [interpolate=true] Use interpolated values when resampling. By default, * the linear interpolation is used to resample the data. If false, nearest neighbor is used. */ /** * @classdesc * A source for working with GeoTIFF data. * **Note for users of the full build**: The `GeoTIFF` source requires the * [geotiff.js](https://github.com/geotiffjs/geotiff.js) library to be loaded as well. * * @api */ class GeoTIFFSource extends DataTile { /** * @param {Options} options Data tile options. */ constructor(options) { super({ state: 'loading', tileGrid: null, projection: null, opaque: options.opaque, transition: options.transition, interpolate: options.interpolate !== false, wrapX: options.wrapX, }); /** * @type {Array} * @private */ this.sourceInfo_ = options.sources; const numSources = this.sourceInfo_.length; /** * @type {Object} * @private */ this.sourceOptions_ = options.sourceOptions; /** * @type {Array>} * @private */ this.sourceImagery_ = new Array(numSources); /** * @type {Array>} * @private */ this.sourceMasks_ = new Array(numSources); /** * @type {Array} * @private */ this.resolutionFactors_ = new Array(numSources); /** * @type {Array} * @private */ this.samplesPerPixel_; /** * @type {Array>} * @private */ this.nodataValues_; /** * @type {Array>} * @private */ this.metadata_; /** * @type {boolean} * @private */ this.normalize_ = options.normalize !== false; /** * @type {boolean} * @private */ this.addAlpha_ = false; /** * @type {Error} * @private */ this.error_ = null; /** * @type {true|false|'auto'} */ this.convertToRGB_ = options.convertToRGB || false; this.setKey(this.sourceInfo_.map((source) => source.url).join(',')); const self = this; const requests = new Array(numSources); for (let i = 0; i < numSources; ++i) { requests[i] = getImagesForSource( this.sourceInfo_[i], this.sourceOptions_ ); } Promise.all(requests) .then(function (sources) { self.configure_(sources); }) .catch(function (error) { logError(error); self.error_ = error; self.setState('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() { return this.error_; } /** * Determine the projection of the images in this GeoTIFF. * The default implementation looks at the ProjectedCSTypeGeoKey and the GeographicTypeGeoKey * of each image in turn. * You can override this method in a subclass to support more projections. * * @param {Array>} sources Each source is a list of images * from a single GeoTIFF. */ determineProjection(sources) { const firstSource = sources[0]; for (let i = firstSource.length - 1; i >= 0; --i) { const image = firstSource[i]; const projection = getProjection(image); if (projection) { this.projection = projection; break; } } } /** * Configure the tile grid based on images within the source GeoTIFFs. Each GeoTIFF * must have the same internal tiled structure. * @param {Array>} sources Each source is a list of images * from a single GeoTIFF. * @private */ configure_(sources) { let extent; let origin; let commonRenderTileSizes; let commonSourceTileSizes; let resolutions; const samplesPerPixel = new Array(sources.length); const nodataValues = new Array(sources.length); const metadata = new Array(sources.length); let minZoom = 0; const sourceCount = sources.length; for (let sourceIndex = 0; sourceIndex < sourceCount; ++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; const sourceTileSizes = new Array(imageCount); const renderTileSizes = new Array(imageCount); const sourceResolutions = new Array(imageCount); nodataValues[sourceIndex] = new Array(imageCount); metadata[sourceIndex] = new Array(imageCount); for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) { const image = images[imageIndex]; const nodataValue = image.getGDALNoData(); metadata[sourceIndex][imageIndex] = image.getGDALMetadata(0); nodataValues[sourceIndex][imageIndex] = nodataValue; const wantedSamples = this.sourceInfo_[sourceIndex].bands; samplesPerPixel[sourceIndex] = wantedSamples ? wantedSamples.length : image.getSamplesPerPixel(); const level = imageCount - (imageIndex + 1); if (!sourceExtent) { sourceExtent = getBoundingBox(image); } if (!sourceOrigin) { sourceOrigin = getOrigin(image); } const imageResolutions = getResolutions(image, images[0]); sourceResolutions[level] = imageResolutions[0]; const sourceTileSize = [image.getTileWidth(), image.getTileHeight()]; // request larger blocks for untiled layouts if ( sourceTileSize[0] !== sourceTileSize[1] && sourceTileSize[1] < defaultTileSize ) { sourceTileSize[0] = defaultTileSize; sourceTileSize[1] = defaultTileSize; } sourceTileSizes[level] = sourceTileSize; const aspectRatio = imageResolutions[0] / Math.abs(imageResolutions[1]); renderTileSizes[level] = [ sourceTileSize[0], sourceTileSize[1] / aspectRatio, ]; } if (!extent) { extent = sourceExtent; } else { getIntersection(extent, sourceExtent, extent); } if (!origin) { origin = sourceOrigin; } else { const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`; assertEqual(origin, sourceOrigin, 0, message, this.viewRejector); } 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.02, message, this.viewRejector ); } if (!commonRenderTileSizes) { commonRenderTileSizes = renderTileSizes; } else { assertEqual( commonRenderTileSizes.slice(minZoom, commonRenderTileSizes.length), renderTileSizes, 0.01, `Tile size mismatch for source ${sourceIndex}`, this.viewRejector ); } if (!commonSourceTileSizes) { commonSourceTileSizes = sourceTileSizes; } else { assertEqual( commonSourceTileSizes.slice(minZoom, commonSourceTileSizes.length), sourceTileSizes, 0, `Tile size mismatch for source ${sourceIndex}`, this.viewRejector ); } this.sourceImagery_[sourceIndex] = images.reverse(); this.sourceMasks_[sourceIndex] = masks.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()) { this.determineProjection(sources); } this.samplesPerPixel_ = samplesPerPixel; this.nodataValues_ = nodataValues; this.metadata_ = metadata; // decide if we need to add an alpha band to handle nodata outer: for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { // option 1: source is configured with a nodata value if (this.sourceInfo_[sourceIndex].nodata !== undefined) { this.addAlpha_ = true; break; } if (this.sourceMasks_[sourceIndex].length) { this.addAlpha_ = true; break; } const values = nodataValues[sourceIndex]; // option 2: check image metadata for limited bands const bands = this.sourceInfo_[sourceIndex].bands; if (bands) { for (let i = 0; i < bands.length; ++i) { if (values[bands[i] - 1] !== null) { this.addAlpha_ = true; break outer; } } continue; } // option 3: check image metadata for all bands for (let imageIndex = 0; imageIndex < values.length; ++imageIndex) { if (values[imageIndex] !== null) { this.addAlpha_ = true; break outer; } } } let bandCount = this.addAlpha_ ? 1 : 0; for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { bandCount += samplesPerPixel[sourceIndex]; } this.bandCount = bandCount; const tileGrid = new TileGrid({ extent: extent, minZoom: minZoom, origin: origin, resolutions: resolutions, tileSizes: commonRenderTileSizes, }); this.tileGrid = tileGrid; this.setTileSizes(commonSourceTileSizes); this.setLoader(this.loadTile_.bind(this)); this.setState('ready'); const zoom = 1; if (resolutions.length === 2) { resolutions = [resolutions[0], resolutions[1], resolutions[1] / 2]; } else if (resolutions.length === 1) { resolutions = [resolutions[0] * 2, resolutions[0], resolutions[0] / 2]; } this.viewResolver({ showFullExtent: true, projection: this.projection, resolutions: resolutions, center: toUserCoordinate(getCenter(extent), this.projection), extent: toUserExtent(extent, this.projection), zoom: zoom, }); } /** * @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 * 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]; const pixelBounds = [ Math.round(x * (sourceTileSize[0] * resolutionFactor)), Math.round(y * (sourceTileSize[1] * resolutionFactor)), Math.round((x + 1) * (sourceTileSize[0] * resolutionFactor)), Math.round((y + 1) * (sourceTileSize[1] * resolutionFactor)), ]; const image = this.sourceImagery_[sourceIndex][z]; let samples; if (source.bands) { samples = source.bands.map(function (bandNumber) { return bandNumber - 1; }); } /** @type {number|Array} */ let fillValue; if ('nodata' in source && source.nodata !== null) { fillValue = source.nodata; } else { if (!samples) { fillValue = nodataValues[sourceIndex]; } else { fillValue = samples.map(function (sampleIndex) { return nodataValues[sourceIndex][sampleIndex]; }); } } const readOptions = { window: pixelBounds, width: sourceTileSize[0], height: sourceTileSize[1], samples: samples, fillValue: fillValue, pool: pool, interleave: false, }; if (readRGB(this.convertToRGB_, image)) { requests[sourceIndex] = image.readRGB(readOptions); } else { requests[sourceIndex] = image.readRasters(readOptions); } // 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) { logError(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; } } /** * Get a promise for view properties based on the source. Use the result of this function * as the `view` option in a map constructor. * * const source = new GeoTIFF(options); * * const map = new Map({ * target: 'map', * layers: [ * new TileLayer({ * source: source, * }), * ], * view: source.getView(), * }); * * @function * @return {Promise} A promise for view-related properties. * @api * */ GeoTIFFSource.prototype.getView; export default GeoTIFFSource;