UNPKG

ol

Version:

OpenLayers mapping library

473 lines (423 loc) • 14.9 kB
/** * @module ol/source/GeoZarr */ import {FetchStore, get, open, slice} from 'zarrita'; import {getCenter} from '../extent.js'; import {get as getProjection, toUserCoordinate, toUserExtent} from '../proj.js'; import {toSize} from '../size.js'; import WMTSTileGrid from '../tilegrid/WMTS.js'; import DataTileSource from './DataTile.js'; import {parseTileMatrixSet} from './ogcTileUtil.js'; const REQUIRED_ZARR_CONVENTIONS = [ 'd35379db-88df-4056-af3a-620245f8e347', // multisacles 'f17cb550-5864-4468-aeb7-f3180cfb622f', // proj: '689b58e2-cf7b-45e0-9fff-9cfc0883d6b4', // spatial: ]; /** * @typedef {'nearest'|'linear'} ResampleMethod */ /** * @typedef {Object} Options * @property {string} url The Zarr URL. * @property {string} group The group with arrays to render. * @property {Array<string>} bands The band names to render. * @property {import("../proj.js").ProjectionLike} [projection] Source projection. If not provided, the GeoTIFF metadata * will be read for projection information. * @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 {ResampleMethod} [resample='nearest'] Resamplilng method if bands are not available for all multi-scale levels. */ export default class GeoZarr extends DataTileSource { /** * @param {Options} options The options. */ constructor(options) { super({ state: 'loading', tileGrid: null, projection: options.projection || null, transition: options.transition, wrapX: options.wrapX, }); /** * @type {string} */ this.url_ = options.url; /** * @type {string} */ this.group_ = options.group; /** * @type {Error|null} */ this.error_ = null; /** * @type {import('zarrita').Group<any>} */ this.root_ = null; /** * @type {any|null} */ this.consolidatedMetadata_ = null; /** * @type {Array<string>} */ this.bands_ = options.bands; /** * @type {Object<string, Array<string>> | null} */ this.bandsByLevel_ = null; /** * @type {number|undefined} */ this.fillValue_; /** * @type {ResampleMethod} */ this.resampleMethod_ = options.resample || 'linear'; /** * @type {number} Number of bands. */ this.bandCount = this.bands_.length; this.setLoader(this.loadTile_.bind(this)); /** * @type {import("../tilegrid/WMTS.js").default} */ this.tileGrid; this.configure_() .then(() => { this.setState('ready'); }) .catch((err) => { this.error_ = err; this.setState('error'); }); } async configure_() { const store = new FetchStore(this.url_); this.root_ = await open(store, {kind: 'group'}); try { this.consolidatedMetadata_ = JSON.parse( new TextDecoder().decode( await store.get(this.root_.resolve('zarr.json').path), ), ).consolidated_metadata.metadata; } catch { // empty catch block } const group = await open(this.root_.resolve(this.group_), {kind: 'group'}); const attributes = /** @type {LegacyDatasetAttributes | DatasetAttributes} */ (group.attrs); if ( 'zarr_conventions' in attributes && Array.isArray(attributes.zarr_conventions) && REQUIRED_ZARR_CONVENTIONS.every((uuid) => attributes.zarr_conventions.find((c) => c.uuid === uuid), ) && 'layout' in attributes.multiscales ) { const {tileGrid, projection, bandsByLevel, fillValue} = getTileGridInfoFromAttributes( /** @type {DatasetAttributes} */ (attributes), this.consolidatedMetadata_, this.group_, this.bands_, ); this.bandsByLevel_ = bandsByLevel; this.tileGrid = tileGrid; this.projection = projection; this.fillValue_ = fillValue; } if ('tile_matrix_set' in attributes.multiscales) { // If available, use tile_matrix_set (legacy attributes) to get a tile grid, because it // provides a better mapping of tiles to zarr chunks. const {tileGrid, projection} = getTileGridInfoFromLegacyAttributes( /** @type {LegacyDatasetAttributes} */ (attributes), ); this.tileGrid = tileGrid; if (!this.projection) { // If there were no required zarr conventions, we don't have a projection yet this.projection = projection; } } if (!this.tileGrid) { throw new Error('Could not determine tile grid'); } const extent = this.tileGrid.getExtent(); setTimeout(() => { this.viewResolver({ showFullExtent: true, projection: this.projection, resolutions: this.tileGrid.getResolutions(), center: toUserCoordinate(getCenter(extent), this.projection), extent: toUserExtent(extent, this.projection), zoom: 1, }); }); } /** * @param {number} z The z tile index. * @param {number} x The x tile index. * @param {number} y The y tile index. * @param {import('./DataTile.js').LoaderOptions} options The loader options. * @return {Promise} The composed tile data. * @private */ async loadTile_(z, x, y, options) { const resolutions = this.tileGrid.getResolutions(); const tileResolution = this.tileGrid.getResolution(z); const tileExtent = this.tileGrid.getTileCoordExtent([z, x, y]); const bandPromises = []; const bandResolutions = []; for (const band of this.bands_) { let bandMatrixId; let bandResolution; let bandZ = 0; if (!this.bandsByLevel_) { // TODO: remove this if we stop supporting legacy attributes bandMatrixId = this.tileGrid.getMatrixId(z); bandResolution = tileResolution; bandZ = z; } else { for ( let candidateZ = 0; candidateZ < resolutions.length; candidateZ += 1 ) { const candidateResolution = resolutions[candidateZ]; if (bandMatrixId && candidateResolution < tileResolution) { break; } const candidateMatrixId = this.tileGrid.getMatrixId(candidateZ); if (this.bandsByLevel_[candidateMatrixId].includes(band)) { bandMatrixId = candidateMatrixId; bandResolution = this.tileGrid.getResolution(candidateZ); bandZ = candidateZ; } } } if (!bandMatrixId || !bandResolution) { throw new Error(`Could not find available resolution for band ${band}`); } const origin = this.tileGrid.getOrigin(bandZ); const minCol = Math.round((tileExtent[0] - origin[0]) / bandResolution); const maxCol = Math.round((tileExtent[2] - origin[0]) / bandResolution); const minRow = Math.round((origin[1] - tileExtent[3]) / bandResolution); const maxRow = Math.round((origin[1] - tileExtent[1]) / bandResolution); const path = `${this.group_}/${bandMatrixId}/${band}`; const array = await open(this.root_.resolve(path), {kind: 'array'}); bandPromises.push( get(array, [slice(minRow, maxRow), slice(minCol, maxCol)]), ); bandResolutions.push(bandResolution); } const bandChunks = await Promise.all(bandPromises); const [tileColCount, tileRowCount] = toSize(this.tileGrid.getTileSize(z)); return composeData( bandChunks, bandResolutions, tileColCount, tileRowCount, tileResolution, this.resampleMethod_, this.fillValue_ || 0, ); } } /** * @typedef {Object} DatasetAttributes * @property {Multiscales} multiscales The multiscales attribute. * @property {Array<{uuid: string}>} zarr_conventions The zarr conventions attribute. */ /** * @typedef {Object} Multiscales * @property {Object} layout The layout. */ /** * @typedef {Object} LegacyDatasetAttributes * @property {LegacyMultiscales} multiscales The multiscales attribute. */ /** * @typedef {Object} LegacyMultiscales * @property {any} tile_matrix_limits The tile matrix limits. * @property {any} tile_matrix_set The tile matrix set. */ /** * @typedef {Object} TileGridInfo * @property {WMTSTileGrid} tileGrid The tile grid. * @property {import("../proj/Projection.js").default} projection The projection. * @property {Object<string, Array<string>>} [bandsByLevel] Available bands by level. * @property {number} [fillValue] The fill value. */ /** * @param {DatasetAttributes} attributes The dataset attributes. * @param {any} consolidatedMetadata The consolidated metadata. * @param {string} wantedGroup The path to the wanted group. * @param {Array<string>} wantedBands The wanted bands. * @return {TileGridInfo} The tile grid info. */ function getTileGridInfoFromAttributes( attributes, consolidatedMetadata, wantedGroup, wantedBands, ) { const multiscales = attributes.multiscales; const extent = attributes['spatial:bbox']; const projection = getProjection(attributes['proj:code']); /** @type {Array<{matrixId: string, resolution: number, origin: import("ol/coordinate").Coordinate}>} */ const groupInfo = []; const bandsByLevel = consolidatedMetadata ? {} : null; let fillValue; for (const groupMetadata of multiscales.layout) { //TODO Handle the complete transform (rotation and different x/y resolutions) const transform = groupMetadata['spatial:transform']; const resolution = transform[0]; const origin = [transform[2], transform[5]]; const matrixId = groupMetadata.asset; groupInfo.push({ matrixId, resolution, origin, }); if (consolidatedMetadata) { const availableBands = []; for (const band of wantedBands) { const bandArray = consolidatedMetadata[`${wantedGroup}/${matrixId}/${band}`]; if (bandArray) { availableBands.push(band); if (fillValue === undefined) { fillValue = bandArray['fill_value']; } } } bandsByLevel[matrixId] = availableBands; } } groupInfo.sort((a, b) => b.resolution - a.resolution); const tileGrid = new WMTSTileGrid({ extent: extent, origins: groupInfo.map((g) => g.origin), resolutions: groupInfo.map((g) => g.resolution), matrixIds: groupInfo.map((g) => g.matrixId), }); return {tileGrid, projection, bandsByLevel, fillValue}; } /** * @param {LegacyDatasetAttributes} attributes The dataset attributes. * @return {TileGridInfo} The tile grid info. */ function getTileGridInfoFromLegacyAttributes(attributes) { const multiscales = attributes.multiscales; const tileMatrixSet = multiscales.tile_matrix_set; const tileMatrixLimitsObject = multiscales.tile_matrix_limits; const numMatrices = tileMatrixSet.tileMatrices.length; const tileMatrixLimits = new Array(numMatrices); let overrideTileSize = false; for (let i = 0; i < numMatrices; i += 1) { const tileMatrix = tileMatrixSet.tileMatrices[i]; const tilematrixId = tileMatrix.id; if (tileMatrix.tileWidth > 512 || tileMatrix.tileHeight > 512) { // Avoid tile sizes that are too large for rendering overrideTileSize = true; } tileMatrixLimits[i] = tileMatrixLimitsObject[tilematrixId]; } const info = parseTileMatrixSet( {}, tileMatrixSet, undefined, tileMatrixLimits, ); let tileGrid = info.grid; // Tile size sanity if (overrideTileSize) { tileGrid = new WMTSTileGrid({ tileSize: 512, extent: tileGrid.getExtent(), origins: tileGrid.getOrigins(), resolutions: tileGrid.getResolutions(), matrixIds: tileGrid.getMatrixIds(), }); } return {tileGrid, projection: info.projection}; } /** * @param {Array<import("zarrita").Chunk<import("zarrita").DataType>>} chunks The input chunks. * @param {Array<number>} chunkResolutions The resolutions for each band. * @param {number} tileColCount The number of columns in the output data. * @param {number} tileRowCount The number of rows in the output data. * @param {number} tileResolution The tile resolution. * @param {ResampleMethod} resampleMethod The resampling method. * @param {number} fillValue The fill value. * @return {Float32Array} The tile data. */ function composeData( chunks, chunkResolutions, tileColCount, tileRowCount, tileResolution, resampleMethod, fillValue, ) { const bandCount = chunks.length; const tileData = new Float32Array(tileColCount * tileRowCount * bandCount); for (let tileRow = 0; tileRow < tileRowCount; tileRow++) { for (let tileCol = 0; tileCol < tileColCount; tileCol++) { for (let band = 0; band < bandCount; ++band) { const chunk = chunks[band]; const chunkRowCount = chunk.shape[0]; const chunkColCount = chunk.shape[1]; const scaleFactor = tileResolution / chunkResolutions[band]; let value = fillValue; if (scaleFactor === 1) { if (tileRow < chunkRowCount && tileCol < chunkColCount) { value = chunk.data[tileRow * chunkColCount + tileCol]; } } else { const chunkRow = tileRow * scaleFactor; const chunkCol = tileCol * scaleFactor; switch (resampleMethod) { case 'nearest': { const valueRow = Math.round(chunkRow); const valueCol = Math.round(chunkCol); if (valueRow < chunkRowCount && valueCol < chunkColCount) { value = chunk.data[valueRow * chunkColCount + valueCol]; } break; } case 'linear': { const row0 = Math.floor(chunkRow); const col0 = Math.floor(chunkCol); if (row0 < chunkRowCount && col0 < chunkColCount) { const row1 = Math.min(row0 + 1, chunkRowCount - 1); const col1 = Math.min(col0 + 1, chunkColCount - 1); const v00 = chunk.data[row0 * chunkColCount + col0]; const v01 = chunk.data[row0 * chunkColCount + col1]; const v10 = chunk.data[row1 * chunkColCount + col0]; const v11 = chunk.data[row1 * chunkColCount + col1]; const dx = chunkCol - col0; const dy = chunkRow - row0; value = (1 - dy) * ((1 - dx) * v00 + dx * v01) + dy * ((1 - dx) * v10 + dx * v11); } break; } default: { throw new Error(`Unsupported resample method: ${resampleMethod}`); } } } if (isNaN(value)) { value = fillValue; } tileData[bandCount * (tileRow * tileColCount + tileCol) + band] = value; } } } return tileData; }