@@ -134,23 +134,34 @@ export default class GeoZarr extends DataTileSource {
134134 async configure_ ( ) {
135135 const store = new FetchStore ( this . url_ ) ;
136136
137- this . root_ = await open ( store , { kind : 'group' } ) ;
138-
139- try {
140- this . consolidatedMetadata_ = JSON . parse (
141- new TextDecoder ( ) . decode (
142- await store . get ( this . root_ . resolve ( 'zarr.json' ) . path ) ,
143- ) ,
144- ) . consolidated_metadata . metadata ;
145- } catch {
146- // empty catch block
137+ // Fetch root zarr.json once for both opening the root and extracting
138+ // consolidated metadata. Without this, open() and the manual metadata
139+ // read would each make a separate HTTP request for the same file.
140+ const rootBytes = await store . get ( '/zarr.json' ) ;
141+ if ( rootBytes ) {
142+ try {
143+ this . consolidatedMetadata_ = JSON . parse (
144+ new TextDecoder ( ) . decode ( rootBytes ) ,
145+ ) . consolidated_metadata . metadata ;
146+ } catch {
147+ // no consolidated metadata
148+ }
147149 }
148150
151+ // Wrap the store so that child metadata (groups, arrays) is served from
152+ // the consolidated metadata instead of making per-child HTTP requests.
153+ const cachedStore = this . consolidatedMetadata_
154+ ? createCachedStore ( store , rootBytes , this . consolidatedMetadata_ )
155+ : store ;
156+
157+ this . root_ = await open ( cachedStore , { kind : 'group' } ) ;
158+
149159 const group = await open ( this . root_ . resolve ( this . group_ ) , { kind : 'group' } ) ;
150160
151161 const attributes =
152162 /** @type {LegacyDatasetAttributes | DatasetAttributes } */ ( group . attrs ) ;
153163
164+ let hasTileSizes = false ;
154165 if (
155166 'zarr_conventions' in attributes &&
156167 Array . isArray ( attributes . zarr_conventions ) &&
@@ -159,7 +170,7 @@ export default class GeoZarr extends DataTileSource {
159170 ) &&
160171 'layout' in attributes . multiscales
161172 ) {
162- const { tileGrid, projection, bandsByLevel, fillValue} =
173+ const { tileGrid, projection, bandsByLevel, fillValue, tileSizes } =
163174 getTileGridInfoFromAttributes (
164175 /** @type {DatasetAttributes } */ ( attributes ) ,
165176 this . consolidatedMetadata_ ,
@@ -170,14 +181,11 @@ export default class GeoZarr extends DataTileSource {
170181 this . tileGrid = tileGrid ;
171182 this . projection = projection ;
172183 this . fillValue_ = fillValue ;
173- if ( this . fillValue_ !== null && this . fillValue_ !== undefined ) {
174- this . bandCount = this . bands_ . length + 1 ;
175- this . nodataBandIndex = this . bandCount ;
176- }
184+ hasTileSizes = ! ! tileSizes ;
177185 }
178- if ( 'tile_matrix_set' in attributes . multiscales ) {
186+ if ( ! hasTileSizes && 'tile_matrix_set' in attributes . multiscales ) {
179187 // If available, use tile_matrix_set (legacy attributes) to get a tile grid, because it
180- // provides a better mapping of tiles to zarr chunks.
188+ // should provide a better mapping of tiles to zarr chunks.
181189 const { tileGrid, projection} = getTileGridInfoFromLegacyAttributes (
182190 /** @type {LegacyDatasetAttributes } */ ( attributes ) ,
183191 ) ;
@@ -187,6 +195,10 @@ export default class GeoZarr extends DataTileSource {
187195 this . projection = projection ;
188196 }
189197 }
198+ if ( this . fillValue_ !== null && this . fillValue_ !== undefined ) {
199+ this . bandCount = this . bands_ . length + 1 ;
200+ this . nodataBandIndex = this . bandCount ;
201+ }
190202 if ( ! this . tileGrid ) {
191203 throw new Error ( 'Could not determine tile grid' ) ;
192204 }
@@ -310,6 +322,32 @@ export default class GeoZarr extends DataTileSource {
310322 }
311323}
312324
325+ /**
326+ * Create a store wrapper that serves Zarr v3 metadata from consolidated
327+ * metadata, avoiding per-child HTTP requests.
328+ * @param {import('zarrita').FetchStore } store The underlying store.
329+ * @param {Uint8Array } rootBytes The already-fetched root zarr.json bytes.
330+ * @param {Object } consolidatedMetadata The parsed consolidated_metadata.metadata entries.
331+ * @return {Object } A store-compatible object.
332+ */
333+ function createCachedStore ( store , rootBytes , consolidatedMetadata ) {
334+ const cache = new Map ( ) ;
335+ cache . set ( '/zarr.json' , rootBytes ) ;
336+ const encoder = new TextEncoder ( ) ;
337+ for ( const [ key , value ] of Object . entries ( consolidatedMetadata ) ) {
338+ cache . set ( `/${ key } /zarr.json` , encoder . encode ( JSON . stringify ( value ) ) ) ;
339+ }
340+ return {
341+ async get ( key , opts ) {
342+ if ( cache . has ( key ) ) {
343+ return cache . get ( key ) ;
344+ }
345+ return store . get ( key , opts ) ;
346+ } ,
347+ getRange : store . getRange ?. bind ( store ) ,
348+ } ;
349+ }
350+
313351/**
314352 * @typedef {Object } DatasetAttributes
315353 * @property {Multiscales } multiscales The multiscales attribute.
@@ -338,8 +376,87 @@ export default class GeoZarr extends DataTileSource {
338376 * @property {import("../proj/Projection.js").default } projection The projection.
339377 * @property {Object<string, Array<string>> } [bandsByLevel] Available bands by level.
340378 * @property {number } [fillValue] The fill value.
379+ * @property {Array<import("../size.js").Size>|undefined } [tileSizes] The tile sizes for each level, if available.
341380 */
342381
382+ /**
383+ * Maximum tile size for rendering.
384+ * @type {number }
385+ */
386+ const MAX_TILE_SIZE = 512 ;
387+
388+ /**
389+ * Minimum tile size when sharding is used.
390+ * @type {number }
391+ */
392+ const MIN_TILE_SIZE = 64 ;
393+
394+ /**
395+ * @typedef {Object } ShardInfo
396+ * @property {Array<number> } shardShape The shard (outer chunk) shape [rows, cols].
397+ * @property {Array<number> } innerChunkShape The inner chunk shape [rows, cols].
398+ */
399+
400+ /**
401+ * FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar.
402+ *
403+ * Get the shard and inner chunk shapes from the Zarr v3 array metadata.
404+ * Only returns info when a `sharding_indexed` codec is present, meaning
405+ * `chunk_grid.configuration.chunk_shape` represents the shard (outer chunk) size.
406+ * @param {Object } arrayMeta The Zarr v3 array metadata from consolidated metadata.
407+ * @return {ShardInfo|undefined } The shard info, or undefined.
408+ */
409+ function getShardInfo ( arrayMeta ) {
410+ const chunkGrid = arrayMeta [ 'chunk_grid' ] ;
411+ if ( ! chunkGrid || chunkGrid [ 'name' ] !== 'regular' ) {
412+ return undefined ;
413+ }
414+ const codecs = arrayMeta [ 'codecs' ] ;
415+ if ( ! Array . isArray ( codecs ) ) {
416+ return undefined ;
417+ }
418+ const shardingCodec = codecs . find ( ( c ) => c [ 'name' ] === 'sharding_indexed' ) ;
419+ if ( ! shardingCodec ) {
420+ return undefined ;
421+ }
422+ return {
423+ shardShape : chunkGrid [ 'configuration' ] [ 'chunk_shape' ] ,
424+ innerChunkShape : shardingCodec [ 'configuration' ] [ 'chunk_shape' ] ,
425+ } ;
426+ }
427+
428+ /**
429+ * FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar.
430+ *
431+ * Compute a tile size that is a multiple of the inner chunk size, evenly divides
432+ * the shard size, is at most MAX_TILE_SIZE, and is at least MIN_TILE_SIZE.
433+ * Aligning with inner chunk boundaries avoids fetching the same inner chunk
434+ * data for adjacent tiles.
435+ * @param {number } shardSize The shard size in pixels along one dimension.
436+ * @param {number } innerChunkSize The inner chunk size in pixels along one dimension.
437+ * @return {number } The tile size.
438+ */
439+ function getTileSizeForShard ( shardSize , innerChunkSize ) {
440+ // Find the largest multiple of innerChunkSize that divides shardSize
441+ // and is within [MIN_TILE_SIZE, MAX_TILE_SIZE].
442+ const maxChunks = Math . floor ( MAX_TILE_SIZE / innerChunkSize ) ;
443+ for ( let n = maxChunks ; n >= 1 ; -- n ) {
444+ const candidate = n * innerChunkSize ;
445+ if ( candidate >= MIN_TILE_SIZE && shardSize % candidate === 0 ) {
446+ return candidate ;
447+ }
448+ }
449+ // No ideal size found. Use shard size itself when it fits, otherwise
450+ // use the largest chunk-aligned size that fits within MAX_TILE_SIZE.
451+ if ( shardSize <= MAX_TILE_SIZE && shardSize >= MIN_TILE_SIZE ) {
452+ return shardSize ;
453+ }
454+ if ( shardSize < MIN_TILE_SIZE ) {
455+ return MIN_TILE_SIZE ;
456+ }
457+ return Math . max ( maxChunks * innerChunkSize , MIN_TILE_SIZE ) ;
458+ }
459+
343460/**
344461 * @param {DatasetAttributes } attributes The dataset attributes.
345462 * @param {any } consolidatedMetadata The consolidated metadata.
@@ -356,7 +473,7 @@ function getTileGridInfoFromAttributes(
356473 const multiscales = attributes . multiscales ;
357474 const extent = attributes [ 'spatial:bbox' ] ;
358475 const projection = getProjection ( attributes [ 'proj:code' ] ) ;
359- /** @type {Array<{matrixId: string, resolution: number, origin: import("ol/coordinate").Coordinate}> } */
476+ /** @type {Array<{matrixId: string, resolution: number, origin: import("ol/coordinate").Coordinate, tileSize: import("../size.js").Size|undefined }> } */
360477 const groupInfo = [ ] ;
361478 const bandsByLevel = consolidatedMetadata ? { } : null ;
362479 let fillValue ;
@@ -366,11 +483,8 @@ function getTileGridInfoFromAttributes(
366483 const resolution = transform [ 0 ] ;
367484 const origin = [ transform [ 2 ] , transform [ 5 ] ] ;
368485 const matrixId = groupMetadata . asset ;
369- groupInfo . push ( {
370- matrixId,
371- resolution,
372- origin,
373- } ) ;
486+ /** @type {import("../size.js").Size|undefined } */
487+ let tileSize ;
374488 if ( consolidatedMetadata ) {
375489 const availableBands = [ ] ;
376490 for ( const band of wantedBands ) {
@@ -381,20 +495,47 @@ function getTileGridInfoFromAttributes(
381495 if ( fillValue === undefined ) {
382496 fillValue = Number ( bandArray [ 'fill_value' ] ) ;
383497 }
498+ //FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar
499+ if ( ! tileSize ) {
500+ const shardInfo = getShardInfo ( bandArray ) ;
501+ if ( shardInfo ) {
502+ tileSize = [
503+ getTileSizeForShard (
504+ shardInfo . shardShape [ 1 ] ,
505+ shardInfo . innerChunkShape [ 1 ] ,
506+ ) ,
507+ getTileSizeForShard (
508+ shardInfo . shardShape [ 0 ] ,
509+ shardInfo . innerChunkShape [ 0 ] ,
510+ ) ,
511+ ] ;
512+ }
513+ }
384514 }
385515 }
386516 bandsByLevel [ matrixId ] = availableBands ;
387517 }
518+ groupInfo . push ( {
519+ matrixId,
520+ resolution,
521+ origin,
522+ tileSize,
523+ } ) ;
388524 }
389525 groupInfo . sort ( ( a , b ) => b . resolution - a . resolution ) ;
526+
527+ const tileSizes = groupInfo . map ( ( g ) => g . tileSize ) ;
528+ const hasTileSizes = tileSizes . some ( ( s ) => s !== undefined ) ;
529+
390530 const tileGrid = new WMTSTileGrid ( {
391531 extent : extent ,
392532 origins : groupInfo . map ( ( g ) => g . origin ) ,
393533 resolutions : groupInfo . map ( ( g ) => g . resolution ) ,
394534 matrixIds : groupInfo . map ( ( g ) => g . matrixId ) ,
535+ ...( hasTileSizes ? { tileSizes : tileSizes . map ( ( s ) => s || [ 256 , 256 ] ) } : { } ) ,
395536 } ) ;
396537
397- return { tileGrid, projection, bandsByLevel, fillValue} ;
538+ return { tileGrid, projection, bandsByLevel, fillValue, tileSizes } ;
398539}
399540
400541/**
0 commit comments