Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: Zarr driver
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2021, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "cpl_enumerate.h"
14 : #include "cpl_float.h"
15 : #include "cpl_vsi_virtual.h"
16 : #include "cpl_worker_thread_pool.h"
17 : #include "gdal_thread_pool.h"
18 : #include "zarr.h"
19 : #include "zarr_v3_codec.h"
20 :
21 : #include <algorithm>
22 : #include <atomic>
23 : #include <cassert>
24 : #include <cinttypes>
25 : #include <cmath>
26 : #include <cstdlib>
27 : #include <limits>
28 : #include <map>
29 : #include <set>
30 :
31 : /************************************************************************/
32 : /* ZarrV3Array::ZarrV3Array() */
33 : /************************************************************************/
34 :
35 1324 : ZarrV3Array::ZarrV3Array(
36 : const std::shared_ptr<ZarrSharedResource> &poSharedResource,
37 : const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
38 : const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
39 : const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
40 : const std::vector<GUInt64> &anOuterBlockSize,
41 1324 : const std::vector<GUInt64> &anInnerBlockSize)
42 1324 : : GDALAbstractMDArray(poParent->GetFullName(), osName),
43 : ZarrArray(poSharedResource, poParent, osName, aoDims, oType, aoDtypeElts,
44 1324 : anOuterBlockSize, anInnerBlockSize)
45 : {
46 1324 : }
47 :
48 : /************************************************************************/
49 : /* ZarrV3Array::Create() */
50 : /************************************************************************/
51 :
52 1324 : std::shared_ptr<ZarrV3Array> ZarrV3Array::Create(
53 : const std::shared_ptr<ZarrSharedResource> &poSharedResource,
54 : const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
55 : const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
56 : const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
57 : const std::vector<GUInt64> &anOuterBlockSize,
58 : const std::vector<GUInt64> &anInnerBlockSize)
59 : {
60 : auto arr = std::shared_ptr<ZarrV3Array>(
61 : new ZarrV3Array(poSharedResource, poParent, osName, aoDims, oType,
62 2648 : aoDtypeElts, anOuterBlockSize, anInnerBlockSize));
63 1324 : if (arr->m_nTotalInnerChunkCount == 0)
64 1 : return nullptr;
65 1323 : arr->SetSelf(arr);
66 :
67 1323 : return arr;
68 : }
69 :
70 : /************************************************************************/
71 : /* ~ZarrV3Array() */
72 : /************************************************************************/
73 :
74 2648 : ZarrV3Array::~ZarrV3Array()
75 : {
76 1324 : ZarrV3Array::Flush();
77 2648 : }
78 :
79 : /************************************************************************/
80 : /* Flush() */
81 : /************************************************************************/
82 :
83 5896 : bool ZarrV3Array::Flush()
84 : {
85 5896 : if (!m_bValid)
86 10 : return true;
87 :
88 : // Flush last dirty block (may add to shard write cache)
89 5886 : bool ret = ZarrV3Array::FlushDirtyBlock();
90 :
91 : // Encode and write all cached shards
92 5886 : if (!ZarrV3Array::FlushShardCache())
93 0 : ret = false;
94 :
95 5886 : m_anCachedBlockIndices.clear();
96 :
97 5886 : if (!m_aoDims.empty())
98 : {
99 10822 : for (const auto &poDim : m_aoDims)
100 : {
101 : const auto poZarrDim =
102 7941 : dynamic_cast<const ZarrDimension *>(poDim.get());
103 7941 : if (poZarrDim && poZarrDim->IsXArrayDimension())
104 : {
105 5108 : if (poZarrDim->IsModified())
106 8 : m_bDefinitionModified = true;
107 : }
108 : else
109 : {
110 2833 : break;
111 : }
112 : }
113 : }
114 :
115 5886 : CPLJSONObject oAttrs;
116 11720 : if (m_oAttrGroup.IsModified() || m_bUnitModified || m_bOffsetModified ||
117 11720 : m_bScaleModified || m_bSRSModified)
118 : {
119 52 : m_bNew = false;
120 :
121 52 : oAttrs = SerializeSpecialAttributes();
122 :
123 52 : m_bDefinitionModified = true;
124 : }
125 :
126 5886 : if (m_bDefinitionModified)
127 : {
128 234 : if (!Serialize(oAttrs))
129 1 : ret = false;
130 234 : m_bDefinitionModified = false;
131 : }
132 :
133 5886 : return ret;
134 : }
135 :
136 : /************************************************************************/
137 : /* ZarrV3Array::Serialize() */
138 : /************************************************************************/
139 :
140 234 : bool ZarrV3Array::Serialize(const CPLJSONObject &oAttrs)
141 : {
142 468 : CPLJSONDocument oDoc;
143 468 : CPLJSONObject oRoot = oDoc.GetRoot();
144 :
145 234 : oRoot.Add("zarr_format", 3);
146 234 : oRoot.Add("node_type", "array");
147 :
148 234 : CPLJSONArray oShape;
149 621 : for (const auto &poDim : m_aoDims)
150 : {
151 387 : oShape.Add(static_cast<GInt64>(poDim->GetSize()));
152 : }
153 234 : oRoot.Add("shape", oShape);
154 :
155 234 : oRoot.Add("data_type", m_dtype.ToString());
156 :
157 : {
158 468 : CPLJSONObject oChunkGrid;
159 234 : oRoot.Add("chunk_grid", oChunkGrid);
160 234 : oChunkGrid.Add("name", "regular");
161 468 : CPLJSONObject oConfiguration;
162 234 : oChunkGrid.Add("configuration", oConfiguration);
163 234 : CPLJSONArray oChunks;
164 621 : for (const auto nBlockSize : m_anOuterBlockSize)
165 : {
166 387 : oChunks.Add(static_cast<GInt64>(nBlockSize));
167 : }
168 234 : oConfiguration.Add("chunk_shape", oChunks);
169 : }
170 :
171 : {
172 468 : CPLJSONObject oChunkKeyEncoding;
173 234 : oRoot.Add("chunk_key_encoding", oChunkKeyEncoding);
174 234 : oChunkKeyEncoding.Add("name", m_bV2ChunkKeyEncoding ? "v2" : "default");
175 234 : CPLJSONObject oConfiguration;
176 234 : oChunkKeyEncoding.Add("configuration", oConfiguration);
177 234 : oConfiguration.Add("separator", m_osDimSeparator);
178 : }
179 :
180 234 : if (m_oType.GetClass() == GEDTC_STRING && m_pabyNoData != nullptr)
181 : {
182 1 : char *pStr = nullptr;
183 1 : memcpy(&pStr, m_pabyNoData, sizeof(pStr));
184 1 : oRoot.Add("fill_value", pStr ? pStr : "");
185 : }
186 233 : else if (m_pabyNoData == nullptr)
187 : {
188 405 : if (m_oType.GetNumericDataType() == GDT_Float16 ||
189 405 : m_oType.GetNumericDataType() == GDT_Float32 ||
190 169 : m_oType.GetNumericDataType() == GDT_Float64)
191 : {
192 63 : oRoot.Add("fill_value", "NaN");
193 : }
194 : else
195 : {
196 140 : oRoot.AddNull("fill_value");
197 : }
198 : }
199 : else
200 : {
201 60 : if (m_oType.GetNumericDataType() == GDT_CFloat16 ||
202 60 : m_oType.GetNumericDataType() == GDT_CFloat32 ||
203 22 : m_oType.GetNumericDataType() == GDT_CFloat64)
204 : {
205 : double adfNoDataValue[2];
206 16 : GDALCopyWords(m_pabyNoData, m_oType.GetNumericDataType(), 0,
207 : adfNoDataValue, GDT_CFloat64, 0, 1);
208 16 : CPLJSONArray oArray;
209 48 : for (int i = 0; i < 2; ++i)
210 : {
211 32 : if (std::isnan(adfNoDataValue[i]))
212 6 : oArray.Add("NaN");
213 26 : else if (adfNoDataValue[i] ==
214 26 : std::numeric_limits<double>::infinity())
215 4 : oArray.Add("Infinity");
216 44 : else if (adfNoDataValue[i] ==
217 22 : -std::numeric_limits<double>::infinity())
218 4 : oArray.Add("-Infinity");
219 : else
220 18 : oArray.Add(adfNoDataValue[i]);
221 : }
222 16 : oRoot.Add("fill_value", oArray);
223 : }
224 : else
225 : {
226 14 : SerializeNumericNoData(oRoot);
227 : }
228 : }
229 :
230 234 : if (m_poCodecs)
231 : {
232 218 : oRoot.Add("codecs", m_poCodecs->GetJSon());
233 : }
234 :
235 234 : oRoot.Add("attributes", oAttrs);
236 :
237 : // Set dimension_names
238 234 : if (!m_aoDims.empty())
239 : {
240 430 : CPLJSONArray oDimensions;
241 586 : for (const auto &poDim : m_aoDims)
242 : {
243 : const auto poZarrDim =
244 387 : dynamic_cast<const ZarrDimension *>(poDim.get());
245 387 : if (poZarrDim && poZarrDim->IsXArrayDimension())
246 : {
247 371 : oDimensions.Add(poDim->GetName());
248 : }
249 : else
250 : {
251 16 : oDimensions = CPLJSONArray();
252 16 : break;
253 : }
254 : }
255 215 : if (oDimensions.Size() > 0)
256 : {
257 199 : oRoot.Add("dimension_names", oDimensions);
258 : }
259 : }
260 :
261 : // TODO: codecs
262 :
263 234 : const bool bRet = oDoc.Save(m_osFilename);
264 234 : if (bRet)
265 233 : m_poSharedResource->SetZMetadataItem(m_osFilename, oRoot);
266 468 : return bRet;
267 : }
268 :
269 : /************************************************************************/
270 : /* ZarrV3Array::NeedDecodedBuffer() */
271 : /************************************************************************/
272 :
273 17199 : bool ZarrV3Array::NeedDecodedBuffer() const
274 : {
275 34398 : for (const auto &elt : m_aoDtypeElts)
276 : {
277 17199 : if (elt.needByteSwapping)
278 : {
279 0 : return true;
280 : }
281 : }
282 17199 : return false;
283 : }
284 :
285 : /************************************************************************/
286 : /* ZarrV3Array::AllocateWorkingBuffers() */
287 : /************************************************************************/
288 :
289 993 : bool ZarrV3Array::AllocateWorkingBuffers() const
290 : {
291 993 : if (m_bAllocateWorkingBuffersDone)
292 77 : return m_bWorkingBuffersOK;
293 :
294 916 : m_bAllocateWorkingBuffersDone = true;
295 :
296 916 : size_t nSizeNeeded = m_nInnerBlockSizeBytes;
297 916 : if (NeedDecodedBuffer())
298 : {
299 0 : size_t nDecodedBufferSize = m_oType.GetSize();
300 0 : for (const auto &nBlockSize : m_anInnerBlockSize)
301 : {
302 0 : if (nDecodedBufferSize > std::numeric_limits<size_t>::max() /
303 0 : static_cast<size_t>(nBlockSize))
304 : {
305 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
306 0 : return false;
307 : }
308 0 : nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
309 : }
310 0 : if (nSizeNeeded >
311 0 : std::numeric_limits<size_t>::max() - nDecodedBufferSize)
312 : {
313 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
314 0 : return false;
315 : }
316 0 : nSizeNeeded += nDecodedBufferSize;
317 : }
318 :
319 : // Reserve a buffer for tile content
320 916 : if (nSizeNeeded > 1024 * 1024 * 1024 &&
321 0 : !CPLTestBool(CPLGetConfigOption("ZARR_ALLOW_BIG_TILE_SIZE", "NO")))
322 : {
323 0 : CPLError(CE_Failure, CPLE_AppDefined,
324 : "Zarr tile allocation would require " CPL_FRMT_GUIB " bytes. "
325 : "By default the driver limits to 1 GB. To allow that memory "
326 : "allocation, set the ZARR_ALLOW_BIG_TILE_SIZE configuration "
327 : "option to YES.",
328 : static_cast<GUIntBig>(nSizeNeeded));
329 0 : return false;
330 : }
331 :
332 916 : m_bWorkingBuffersOK =
333 916 : AllocateWorkingBuffers(m_abyRawBlockData, m_abyDecodedBlockData);
334 916 : return m_bWorkingBuffersOK;
335 : }
336 :
337 15768 : bool ZarrV3Array::AllocateWorkingBuffers(
338 : ZarrByteVectorQuickResize &abyRawBlockData,
339 : ZarrByteVectorQuickResize &abyDecodedBlockData) const
340 : {
341 : // This method should NOT modify any ZarrArray member, as it is going to
342 : // be called concurrently from several threads.
343 :
344 : // Set those #define to avoid accidental use of some global variables
345 : #define m_abyRawBlockData cannot_use_here
346 : #define m_abyDecodedBlockData cannot_use_here
347 :
348 15768 : const size_t nSizeNeeded = m_nInnerBlockSizeBytes;
349 : try
350 : {
351 15768 : abyRawBlockData.resize(nSizeNeeded);
352 : }
353 0 : catch (const std::bad_alloc &e)
354 : {
355 0 : CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
356 0 : return false;
357 : }
358 :
359 15768 : if (NeedDecodedBuffer())
360 : {
361 0 : size_t nDecodedBufferSize = m_oType.GetSize();
362 0 : for (const auto &nBlockSize : m_anInnerBlockSize)
363 : {
364 0 : nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
365 : }
366 : try
367 : {
368 0 : abyDecodedBlockData.resize(nDecodedBufferSize);
369 : }
370 0 : catch (const std::bad_alloc &e)
371 : {
372 0 : CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
373 0 : return false;
374 : }
375 : }
376 :
377 15768 : return true;
378 : #undef m_abyRawBlockData
379 : #undef m_abyDecodedBlockData
380 : }
381 :
382 : /************************************************************************/
383 : /* ZarrV3Array::LoadBlockData() */
384 : /************************************************************************/
385 :
386 1793 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices,
387 : bool &bMissingBlockOut) const
388 : {
389 1793 : return LoadBlockData(blockIndices,
390 : false, // use mutex
391 1793 : m_poCodecs.get(), m_abyRawBlockData,
392 3586 : m_abyDecodedBlockData, bMissingBlockOut);
393 : }
394 :
395 16645 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices, bool bUseMutex,
396 : ZarrV3CodecSequence *poCodecs,
397 : ZarrByteVectorQuickResize &abyRawBlockData,
398 : ZarrByteVectorQuickResize &abyDecodedBlockData,
399 : bool &bMissingBlockOut) const
400 : {
401 : // This method should NOT modify any ZarrArray member, as it is going to
402 : // be called concurrently from several threads.
403 :
404 : // Set those #define to avoid accidental use of some global variables
405 : #define m_abyRawBlockData cannot_use_here
406 : #define m_abyDecodedBlockData cannot_use_here
407 : #define m_poCodecs cannot_use_here
408 :
409 16645 : bMissingBlockOut = false;
410 :
411 33290 : std::string osFilename;
412 16645 : if (poCodecs && poCodecs->SupportsPartialDecoding())
413 : {
414 1050 : std::vector<uint64_t> outerChunkIndices;
415 3156 : for (size_t i = 0; i < GetDimensionCount(); ++i)
416 : {
417 : // Note: m_anOuterBlockSize[i]/m_anInnerBlockSize[i] is an integer
418 6318 : outerChunkIndices.push_back(blockIndices[i] *
419 4212 : m_anInnerBlockSize[i] /
420 2106 : m_anOuterBlockSize[i]);
421 : }
422 :
423 1050 : osFilename = BuildChunkFilename(outerChunkIndices.data());
424 : }
425 : else
426 : {
427 15595 : osFilename = BuildChunkFilename(blockIndices);
428 : }
429 :
430 : // For network file systems, get the streaming version of the filename,
431 : // as we don't need arbitrary seeking in the file
432 : // ... unless we do partial decoding, in which case range requests within
433 : // a shard are much more efficient
434 16645 : if (!(poCodecs && poCodecs->SupportsPartialDecoding()))
435 : {
436 15595 : osFilename = VSIFileManager::GetHandler(osFilename.c_str())
437 15595 : ->GetStreamingFilename(osFilename);
438 : }
439 :
440 : // First if we have a tile presence cache, check tile presence from it
441 : bool bEarlyRet;
442 16645 : if (bUseMutex)
443 : {
444 14852 : std::lock_guard<std::mutex> oLock(m_oMutex);
445 14852 : bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
446 : }
447 : else
448 : {
449 1793 : bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
450 : }
451 16645 : if (bEarlyRet)
452 : {
453 26 : bMissingBlockOut = true;
454 26 : return true;
455 : }
456 16619 : VSIVirtualHandleUniquePtr fp;
457 : // This is the number of files returned in a S3 directory listing operation
458 16619 : constexpr uint64_t MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING = 1000;
459 16619 : const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
460 : nullptr};
461 16619 : const auto nErrorBefore = CPLGetErrorCounter();
462 33229 : if ((m_osDimSeparator == "/" && !m_anOuterBlockSize.empty() &&
463 49857 : m_anOuterBlockSize.back() > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING) ||
464 16619 : (m_osDimSeparator != "/" &&
465 9 : m_nTotalInnerChunkCount > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING))
466 : {
467 : // Avoid issuing ReadDir() when a lot of files are expected
468 : CPLConfigOptionSetter optionSetter("GDAL_DISABLE_READDIR_ON_OPEN",
469 0 : "YES", true);
470 0 : fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
471 : }
472 : else
473 : {
474 16619 : fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
475 : }
476 16619 : if (fp == nullptr)
477 : {
478 349 : if (nErrorBefore != CPLGetErrorCounter())
479 : {
480 0 : return false;
481 : }
482 : else
483 : {
484 : // Missing files are OK and indicate nodata_value
485 349 : CPLDebugOnly(ZARR_DEBUG_KEY, "Block %s missing (=nodata)",
486 : osFilename.c_str());
487 349 : bMissingBlockOut = true;
488 349 : return true;
489 : }
490 : }
491 :
492 16270 : bMissingBlockOut = false;
493 :
494 16270 : if (poCodecs && poCodecs->SupportsPartialDecoding())
495 : {
496 1003 : std::vector<size_t> anStartIdx;
497 1003 : std::vector<size_t> anCount;
498 3009 : for (size_t i = 0; i < GetDimensionCount(); ++i)
499 : {
500 2006 : anStartIdx.push_back(
501 4012 : static_cast<size_t>((blockIndices[i] * m_anInnerBlockSize[i]) %
502 2006 : m_anOuterBlockSize[i]));
503 2006 : anCount.push_back(static_cast<size_t>(m_anInnerBlockSize[i]));
504 : }
505 1003 : if (!poCodecs->DecodePartial(fp.get(), abyRawBlockData, anStartIdx,
506 : anCount))
507 356 : return false;
508 : }
509 : else
510 : {
511 15267 : CPLAssert(abyRawBlockData.capacity() >= m_nInnerBlockSizeBytes);
512 : // should not fail
513 15267 : abyRawBlockData.resize(m_nInnerBlockSizeBytes);
514 :
515 15267 : bool bRet = true;
516 15267 : size_t nRawDataSize = abyRawBlockData.size();
517 15267 : if (poCodecs == nullptr)
518 : {
519 6 : nRawDataSize = fp->Read(&abyRawBlockData[0], 1, nRawDataSize);
520 : }
521 : else
522 : {
523 15261 : fp->Seek(0, SEEK_END);
524 15261 : const auto nSize = fp->Tell();
525 15261 : fp->Seek(0, SEEK_SET);
526 15261 : if (nSize >
527 15261 : static_cast<vsi_l_offset>(std::numeric_limits<int>::max()))
528 : {
529 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too large tile %s",
530 : osFilename.c_str());
531 0 : bRet = false;
532 : }
533 : else
534 : {
535 : try
536 : {
537 15261 : abyRawBlockData.resize(static_cast<size_t>(nSize));
538 : }
539 0 : catch (const std::exception &)
540 : {
541 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
542 : "Cannot allocate memory for tile %s",
543 : osFilename.c_str());
544 0 : bRet = false;
545 : }
546 :
547 45782 : if (bRet &&
548 30521 : (abyRawBlockData.empty() ||
549 15260 : fp->Read(&abyRawBlockData[0], 1, abyRawBlockData.size()) !=
550 15260 : abyRawBlockData.size()))
551 : {
552 1 : CPLError(CE_Failure, CPLE_AppDefined,
553 : "Could not read tile %s correctly",
554 : osFilename.c_str());
555 1 : bRet = false;
556 : }
557 : else
558 : {
559 15260 : if (!poCodecs->Decode(abyRawBlockData))
560 : {
561 131 : CPLError(CE_Failure, CPLE_AppDefined,
562 : "Decompression of tile %s failed",
563 : osFilename.c_str());
564 131 : bRet = false;
565 : }
566 : }
567 : }
568 : }
569 15267 : if (!bRet)
570 132 : return false;
571 :
572 15135 : if (nRawDataSize != abyRawBlockData.size())
573 : {
574 0 : CPLError(CE_Failure, CPLE_AppDefined,
575 : "Decompressed tile %s has not expected size. "
576 : "Got %u instead of %u",
577 : osFilename.c_str(),
578 0 : static_cast<unsigned>(abyRawBlockData.size()),
579 : static_cast<unsigned>(nRawDataSize));
580 0 : return false;
581 : }
582 : }
583 :
584 15782 : if (!abyDecodedBlockData.empty())
585 : {
586 : const size_t nSourceSize =
587 0 : m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
588 0 : const auto nDTSize = m_oType.GetSize();
589 0 : const size_t nValues = abyDecodedBlockData.size() / nDTSize;
590 0 : CPLAssert(nValues == m_nInnerBlockSizeBytes / nSourceSize);
591 0 : const GByte *pSrc = abyRawBlockData.data();
592 0 : GByte *pDst = &abyDecodedBlockData[0];
593 0 : for (size_t i = 0; i < nValues;
594 0 : i++, pSrc += nSourceSize, pDst += nDTSize)
595 : {
596 0 : DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
597 : }
598 : }
599 :
600 15782 : return true;
601 :
602 : #undef m_abyRawBlockData
603 : #undef m_abyDecodedBlockData
604 : #undef m_poCodecs
605 : }
606 :
607 : /************************************************************************/
608 : /* ZarrV3Array::IRead() */
609 : /************************************************************************/
610 :
611 856 : bool ZarrV3Array::IRead(const GUInt64 *arrayStartIdx, const size_t *count,
612 : const GInt64 *arrayStep, const GPtrDiff_t *bufferStride,
613 : const GDALExtendedDataType &bufferDataType,
614 : void *pDstBuffer) const
615 : {
616 : // For sharded arrays, pre-populate the block cache via ReadMultiRange()
617 : // so that the base-class block-by-block loop hits memory, not HTTP.
618 856 : if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
619 : {
620 552 : PreloadShardedBlocks(arrayStartIdx, count);
621 : }
622 856 : return ZarrArray::IRead(arrayStartIdx, count, arrayStep, bufferStride,
623 856 : bufferDataType, pDstBuffer);
624 : }
625 :
626 : /************************************************************************/
627 : /* ZarrV3Array::PreloadShardedBlocks() */
628 : /************************************************************************/
629 :
630 556 : void ZarrV3Array::PreloadShardedBlocks(const GUInt64 *arrayStartIdx,
631 : const size_t *count) const
632 : {
633 556 : const size_t nDims = m_aoDims.size();
634 556 : if (nDims == 0)
635 555 : return;
636 :
637 : // Calculate needed block index range
638 1112 : std::vector<uint64_t> anBlockMin(nDims), anBlockMax(nDims);
639 556 : size_t nTotalBlocks = 1;
640 1672 : for (size_t i = 0; i < nDims; ++i)
641 : {
642 1116 : anBlockMin[i] = arrayStartIdx[i] / m_anInnerBlockSize[i];
643 2232 : anBlockMax[i] =
644 1116 : (arrayStartIdx[i] + count[i] - 1) / m_anInnerBlockSize[i];
645 1116 : nTotalBlocks *= static_cast<size_t>(anBlockMax[i] - anBlockMin[i] + 1);
646 : }
647 :
648 556 : if (nTotalBlocks <= 1)
649 33 : return; // single block — no batching benefit
650 :
651 523 : CPLDebugOnly("ZARR", "PreloadShardedBlocks: %" PRIu64 " blocks to batch",
652 : static_cast<uint64_t>(nTotalBlocks));
653 :
654 : // Enumerate all needed blocks, grouped by shard filename
655 : struct BlockInfo
656 : {
657 : std::vector<uint64_t> anBlockIndices{};
658 : std::vector<size_t> anStartIdx{};
659 : std::vector<size_t> anCount{};
660 : };
661 :
662 523 : std::map<std::string, std::vector<BlockInfo>> oShardToBlocks;
663 :
664 : // Iterate over all needed block indices
665 523 : std::vector<uint64_t> anCur(nDims);
666 523 : size_t dimIdx = 0;
667 15915 : lbl_next:
668 15915 : if (dimIdx == nDims)
669 : {
670 : // Skip blocks already in cache
671 25610 : const std::vector<uint64_t> cacheKey(anCur.begin(), anCur.end());
672 12805 : if (m_oChunkCache.find(cacheKey) == m_oChunkCache.end())
673 : {
674 : // Compute shard filename and inner chunk start/count
675 25274 : std::vector<uint64_t> outerIdx(nDims);
676 25274 : BlockInfo info;
677 12637 : info.anBlockIndices = anCur;
678 12637 : info.anStartIdx.resize(nDims);
679 12637 : info.anCount.resize(nDims);
680 37957 : for (size_t i = 0; i < nDims; ++i)
681 : {
682 50640 : outerIdx[i] =
683 25320 : anCur[i] * m_anInnerBlockSize[i] / m_anOuterBlockSize[i];
684 50640 : info.anStartIdx[i] = static_cast<size_t>(
685 25320 : (anCur[i] * m_anInnerBlockSize[i]) % m_anOuterBlockSize[i]);
686 25320 : info.anCount[i] = static_cast<size_t>(m_anInnerBlockSize[i]);
687 : }
688 :
689 25274 : std::string osFilename = BuildChunkFilename(outerIdx.data());
690 12637 : oShardToBlocks[osFilename].push_back(std::move(info));
691 : }
692 : }
693 : else
694 : {
695 3110 : anCur[dimIdx] = anBlockMin[dimIdx];
696 : while (true)
697 : {
698 15392 : dimIdx++;
699 15392 : goto lbl_next;
700 15392 : lbl_return:
701 15392 : dimIdx--;
702 15392 : if (anCur[dimIdx] == anBlockMax[dimIdx])
703 3110 : break;
704 12282 : ++anCur[dimIdx];
705 : }
706 : }
707 15915 : if (dimIdx > 0)
708 15392 : goto lbl_return;
709 :
710 : // Collect shards that qualify for batching (>1 block)
711 : struct ShardWork
712 : {
713 : const std::string *posFilename;
714 : std::vector<BlockInfo> *paBlocks;
715 : };
716 :
717 523 : std::vector<ShardWork> aShardWork;
718 5065 : for (auto &[osFilename, aBlocks] : oShardToBlocks)
719 : {
720 4542 : if (aBlocks.size() > 1)
721 4035 : aShardWork.push_back({&osFilename, &aBlocks});
722 : }
723 :
724 523 : if (aShardWork.empty())
725 8 : return;
726 :
727 515 : const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
728 : nullptr};
729 515 : const bool bNeedDecode = NeedDecodedBuffer();
730 :
731 : // Process one shard: open file, batch-decode, type-convert, cache.
732 : // poCodecs: per-thread clone (parallel) or m_poCodecs (sequential).
733 : // oMutex: guards cache writes (uncontended in sequential path).
734 : const auto ProcessOneShard =
735 4035 : [this, &apszOpenOptions, bNeedDecode](const ShardWork &work,
736 : ZarrV3CodecSequence *poCodecs,
737 29094 : std::mutex &oMutex)
738 : {
739 : VSIVirtualHandleUniquePtr fp(
740 4035 : VSIFOpenEx2L(work.posFilename->c_str(), "rb", 0, apszOpenOptions));
741 4035 : if (!fp)
742 4 : return;
743 :
744 4031 : const auto &aBlocks = *work.paBlocks;
745 : std::vector<std::pair<std::vector<size_t>, std::vector<size_t>>>
746 4031 : anRequests;
747 4031 : anRequests.reserve(aBlocks.size());
748 16147 : for (const auto &info : aBlocks)
749 12116 : anRequests.push_back({info.anStartIdx, info.anCount});
750 :
751 4031 : std::vector<ZarrByteVectorQuickResize> aResults;
752 4031 : if (!poCodecs->BatchDecodePartial(fp.get(), work.posFilename->c_str(),
753 : anRequests, aResults))
754 356 : return;
755 :
756 : // Type-convert outside mutex (CPU-bound, thread-local data)
757 7350 : std::vector<ZarrByteVectorQuickResize> aDecoded;
758 3675 : if (bNeedDecode)
759 : {
760 0 : const size_t nSourceSize = m_aoDtypeElts.back().nativeOffset +
761 0 : m_aoDtypeElts.back().nativeSize;
762 0 : const auto nGDALDTSize = m_oType.GetSize();
763 0 : aDecoded.resize(aBlocks.size());
764 0 : for (size_t i = 0; i < aBlocks.size(); ++i)
765 : {
766 0 : if (aResults[i].empty())
767 0 : continue;
768 0 : const size_t nValues = aResults[i].size() / nSourceSize;
769 0 : aDecoded[i].resize(nValues * nGDALDTSize);
770 0 : const GByte *pSrc = aResults[i].data();
771 0 : GByte *pDst = aDecoded[i].data();
772 0 : for (size_t v = 0; v < nValues;
773 0 : v++, pSrc += nSourceSize, pDst += nGDALDTSize)
774 : {
775 0 : DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
776 : }
777 : }
778 : }
779 :
780 : // Store in cache under mutex
781 7350 : std::lock_guard<std::mutex> oLock(oMutex);
782 14367 : for (size_t i = 0; i < aBlocks.size(); ++i)
783 : {
784 21384 : CachedBlock cachedBlock;
785 10692 : if (!aResults[i].empty())
786 : {
787 10692 : if (bNeedDecode)
788 0 : std::swap(cachedBlock.abyDecoded, aDecoded[i]);
789 : else
790 10692 : std::swap(cachedBlock.abyDecoded, aResults[i]);
791 : }
792 10692 : m_oChunkCache[aBlocks[i].anBlockIndices] = std::move(cachedBlock);
793 : }
794 515 : };
795 :
796 515 : const int nMaxThreads = GDALGetNumThreads();
797 :
798 515 : const int nShards = static_cast<int>(aShardWork.size());
799 515 : std::mutex oMutex;
800 :
801 : // Sequential: single thread, single shard, or no thread pool
802 1 : CPLWorkerThreadPool *wtp = (nMaxThreads > 1 && nShards > 1)
803 516 : ? GDALGetGlobalThreadPool(nMaxThreads)
804 515 : : nullptr;
805 515 : if (!wtp)
806 : {
807 4541 : for (const auto &work : aShardWork)
808 4027 : ProcessOneShard(work, m_poCodecs.get(), oMutex);
809 514 : return;
810 : }
811 :
812 1 : CPLDebugOnly("ZARR",
813 : "PreloadShardedBlocks: parallel across %d shards (%d threads)",
814 : nShards, std::min(nMaxThreads, nShards));
815 :
816 : // Clone codecs upfront on main thread (Clone is not thread-safe)
817 2 : std::vector<std::unique_ptr<ZarrV3CodecSequence>> apoCodecs(nShards);
818 9 : for (int i = 0; i < nShards; ++i)
819 8 : apoCodecs[i] = m_poCodecs->Clone();
820 :
821 2 : auto poQueue = wtp->CreateJobQueue();
822 9 : for (int i = 0; i < nShards; ++i)
823 : {
824 8 : poQueue->SubmitJob([&work = aShardWork[i], pCodecs = apoCodecs[i].get(),
825 8 : &oMutex, &ProcessOneShard]()
826 8 : { ProcessOneShard(work, pCodecs, oMutex); });
827 : }
828 1 : poQueue->WaitCompletion();
829 : }
830 :
831 : /************************************************************************/
832 : /* ZarrV3Array::IAdviseRead() */
833 : /************************************************************************/
834 :
835 25 : bool ZarrV3Array::IAdviseRead(const GUInt64 *arrayStartIdx, const size_t *count,
836 : CSLConstList papszOptions) const
837 : {
838 : // For sharded arrays, batch all needed inner chunks via
839 : // PreloadShardedBlocks (BatchDecodePartial + ReadMultiRange) instead
840 : // of the per-block LoadBlockData path below.
841 25 : if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
842 : {
843 4 : PreloadShardedBlocks(arrayStartIdx, count);
844 4 : return true;
845 : }
846 :
847 42 : std::vector<uint64_t> anIndicesCur;
848 21 : int nThreadsMax = 0;
849 42 : std::vector<uint64_t> anReqBlocksIndices;
850 21 : size_t nReqBlocks = 0;
851 21 : if (!IAdviseReadCommon(arrayStartIdx, count, papszOptions, anIndicesCur,
852 : nThreadsMax, anReqBlocksIndices, nReqBlocks))
853 : {
854 2 : return false;
855 : }
856 19 : if (nThreadsMax <= 1)
857 : {
858 6 : return true;
859 : }
860 :
861 : const int nThreads = static_cast<int>(
862 13 : std::min(static_cast<size_t>(nThreadsMax), nReqBlocks));
863 :
864 13 : CPLWorkerThreadPool *wtp = GDALGetGlobalThreadPool(nThreadsMax);
865 13 : if (wtp == nullptr)
866 0 : return false;
867 :
868 : struct JobStruct
869 : {
870 : JobStruct() = default;
871 :
872 : JobStruct(const JobStruct &) = delete;
873 : JobStruct &operator=(const JobStruct &) = delete;
874 :
875 : JobStruct(JobStruct &&) = default;
876 : JobStruct &operator=(JobStruct &&) = default;
877 :
878 : const ZarrV3Array *poArray = nullptr;
879 : bool *pbGlobalStatus = nullptr;
880 : int *pnRemainingThreads = nullptr;
881 : const std::vector<uint64_t> *panReqBlocksIndices = nullptr;
882 : size_t nFirstIdx = 0;
883 : size_t nLastIdxNotIncluded = 0;
884 : };
885 :
886 13 : std::vector<JobStruct> asJobStructs;
887 :
888 13 : bool bGlobalStatus = true;
889 13 : int nRemainingThreads = nThreads;
890 : // Check for very highly overflow in below loop
891 13 : assert(static_cast<size_t>(nThreads) <
892 : std::numeric_limits<size_t>::max() / nReqBlocks);
893 :
894 : // Setup jobs
895 65 : for (int i = 0; i < nThreads; i++)
896 : {
897 52 : JobStruct jobStruct;
898 52 : jobStruct.poArray = this;
899 52 : jobStruct.pbGlobalStatus = &bGlobalStatus;
900 52 : jobStruct.pnRemainingThreads = &nRemainingThreads;
901 52 : jobStruct.panReqBlocksIndices = &anReqBlocksIndices;
902 52 : jobStruct.nFirstIdx = static_cast<size_t>(i * nReqBlocks / nThreads);
903 52 : jobStruct.nLastIdxNotIncluded = std::min(
904 52 : static_cast<size_t>((i + 1) * nReqBlocks / nThreads), nReqBlocks);
905 52 : asJobStructs.emplace_back(std::move(jobStruct));
906 : }
907 :
908 52 : const auto JobFunc = [](void *pThreadData)
909 : {
910 52 : const JobStruct *jobStruct =
911 : static_cast<const JobStruct *>(pThreadData);
912 :
913 52 : const auto poArray = jobStruct->poArray;
914 52 : const size_t l_nDims = poArray->GetDimensionCount();
915 52 : ZarrByteVectorQuickResize abyRawBlockData;
916 52 : ZarrByteVectorQuickResize abyDecodedBlockData;
917 0 : std::unique_ptr<ZarrV3CodecSequence> poCodecs;
918 52 : if (poArray->m_poCodecs)
919 : {
920 52 : std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
921 52 : poCodecs = poArray->m_poCodecs->Clone();
922 : }
923 :
924 14904 : for (size_t iReq = jobStruct->nFirstIdx;
925 14904 : iReq < jobStruct->nLastIdxNotIncluded; ++iReq)
926 : {
927 : // Check if we must early exit
928 : {
929 14852 : std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
930 14852 : if (!(*jobStruct->pbGlobalStatus))
931 0 : return;
932 : }
933 :
934 : const uint64_t *blockIndices =
935 14852 : jobStruct->panReqBlocksIndices->data() + iReq * l_nDims;
936 :
937 14852 : if (!poArray->AllocateWorkingBuffers(abyRawBlockData,
938 : abyDecodedBlockData))
939 : {
940 0 : std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
941 0 : *jobStruct->pbGlobalStatus = false;
942 0 : break;
943 : }
944 :
945 14852 : bool bIsEmpty = false;
946 14852 : bool success = poArray->LoadBlockData(
947 : blockIndices,
948 : true, // use mutex
949 : poCodecs.get(), abyRawBlockData, abyDecodedBlockData, bIsEmpty);
950 :
951 14852 : std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
952 14852 : if (!success)
953 : {
954 0 : *jobStruct->pbGlobalStatus = false;
955 0 : break;
956 : }
957 :
958 29704 : CachedBlock cachedBlock;
959 14852 : if (!bIsEmpty)
960 : {
961 14850 : if (!abyDecodedBlockData.empty())
962 0 : std::swap(cachedBlock.abyDecoded, abyDecodedBlockData);
963 : else
964 14850 : std::swap(cachedBlock.abyDecoded, abyRawBlockData);
965 : }
966 : const std::vector<uint64_t> cacheKey{blockIndices,
967 29704 : blockIndices + l_nDims};
968 14852 : poArray->m_oChunkCache[cacheKey] = std::move(cachedBlock);
969 : }
970 :
971 52 : std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
972 52 : (*jobStruct->pnRemainingThreads)--;
973 : };
974 :
975 : // Start jobs
976 65 : for (int i = 0; i < nThreads; i++)
977 : {
978 52 : if (!wtp->SubmitJob(JobFunc, &asJobStructs[i]))
979 : {
980 0 : std::lock_guard<std::mutex> oLock(m_oMutex);
981 0 : bGlobalStatus = false;
982 0 : nRemainingThreads = i;
983 0 : break;
984 : }
985 : }
986 :
987 : // Wait for all jobs to be finished
988 : while (true)
989 : {
990 : {
991 53 : std::lock_guard<std::mutex> oLock(m_oMutex);
992 53 : if (nRemainingThreads == 0)
993 13 : break;
994 : }
995 40 : wtp->WaitEvent();
996 40 : }
997 :
998 13 : return bGlobalStatus;
999 : }
1000 :
1001 : /************************************************************************/
1002 : /* ZarrV3Array::FlushDirtyBlock() */
1003 : /************************************************************************/
1004 :
1005 18362 : bool ZarrV3Array::FlushDirtyBlock() const
1006 : {
1007 18362 : if (!m_bDirtyBlock)
1008 7378 : return true;
1009 10984 : m_bDirtyBlock = false;
1010 :
1011 : // Sharded arrays need special handling: the block cache operates at
1012 : // inner chunk granularity but we must write complete shards.
1013 10984 : if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
1014 : {
1015 108 : return FlushDirtyBlockSharded();
1016 : }
1017 :
1018 21752 : std::string osFilename = BuildChunkFilename(m_anCachedBlockIndices.data());
1019 :
1020 : const size_t nSourceSize =
1021 10876 : m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
1022 10876 : const auto &abyBlock = m_abyDecodedBlockData.empty()
1023 : ? m_abyRawBlockData
1024 10876 : : m_abyDecodedBlockData;
1025 :
1026 10876 : if (IsEmptyBlock(abyBlock))
1027 : {
1028 3 : m_bCachedBlockEmpty = true;
1029 :
1030 : VSIStatBufL sStat;
1031 3 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
1032 : {
1033 0 : CPLDebugOnly(ZARR_DEBUG_KEY,
1034 : "Deleting tile %s that has now empty content",
1035 : osFilename.c_str());
1036 0 : return VSIUnlink(osFilename.c_str()) == 0;
1037 : }
1038 3 : return true;
1039 : }
1040 :
1041 10873 : if (!m_abyDecodedBlockData.empty())
1042 : {
1043 0 : const size_t nDTSize = m_oType.GetSize();
1044 0 : const size_t nValues = m_abyDecodedBlockData.size() / nDTSize;
1045 0 : GByte *pDst = &m_abyRawBlockData[0];
1046 0 : const GByte *pSrc = m_abyDecodedBlockData.data();
1047 0 : for (size_t i = 0; i < nValues;
1048 0 : i++, pDst += nSourceSize, pSrc += nDTSize)
1049 : {
1050 0 : EncodeElt(m_aoDtypeElts, pSrc, pDst);
1051 : }
1052 : }
1053 :
1054 10873 : const size_t nSizeBefore = m_abyRawBlockData.size();
1055 10873 : if (m_poCodecs)
1056 : {
1057 10873 : if (!m_poCodecs->Encode(m_abyRawBlockData))
1058 : {
1059 0 : m_abyRawBlockData.resize(nSizeBefore);
1060 0 : return false;
1061 : }
1062 : }
1063 :
1064 10873 : if (m_osDimSeparator == "/")
1065 : {
1066 10873 : std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
1067 : VSIStatBufL sStat;
1068 10873 : if (VSIStatL(osDir.c_str(), &sStat) != 0)
1069 : {
1070 274 : if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
1071 : {
1072 0 : CPLError(CE_Failure, CPLE_AppDefined,
1073 : "Cannot create directory %s", osDir.c_str());
1074 0 : m_abyRawBlockData.resize(nSizeBefore);
1075 0 : return false;
1076 : }
1077 : }
1078 : }
1079 :
1080 10873 : VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
1081 10873 : if (fp == nullptr)
1082 : {
1083 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot create tile %s",
1084 : osFilename.c_str());
1085 0 : m_abyRawBlockData.resize(nSizeBefore);
1086 0 : return false;
1087 : }
1088 :
1089 10873 : bool bRet = true;
1090 10873 : const size_t nRawDataSize = m_abyRawBlockData.size();
1091 10873 : if (VSIFWriteL(m_abyRawBlockData.data(), 1, nRawDataSize, fp) !=
1092 : nRawDataSize)
1093 : {
1094 0 : CPLError(CE_Failure, CPLE_AppDefined,
1095 : "Could not write tile %s correctly", osFilename.c_str());
1096 0 : bRet = false;
1097 : }
1098 10873 : VSIFCloseL(fp);
1099 :
1100 10873 : m_abyRawBlockData.resize(nSizeBefore);
1101 :
1102 10873 : return bRet;
1103 : }
1104 :
1105 : /************************************************************************/
1106 : /* ZarrV3Array::FlushDirtyBlockSharded() */
1107 : /************************************************************************/
1108 :
1109 : // Accumulates dirty inner chunks into a per-shard write cache.
1110 : // Actual encoding and writing happens in FlushShardCache().
1111 : // This avoids the O(N) decode-encode cost of re-encoding the full shard
1112 : // for every inner chunk write (N = inner chunks per shard).
1113 : // Single-writer only: concurrent writes to the same shard are not supported.
1114 :
1115 108 : bool ZarrV3Array::FlushDirtyBlockSharded() const
1116 : {
1117 108 : const size_t nDims = GetDimensionCount();
1118 : const size_t nSourceSize =
1119 108 : m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
1120 :
1121 : // 1. Convert dirty inner block from GDAL format to native format
1122 108 : if (!m_abyDecodedBlockData.empty())
1123 : {
1124 0 : const size_t nDTSize = m_oType.GetSize();
1125 0 : const size_t nValues = m_abyDecodedBlockData.size() / nDTSize;
1126 0 : GByte *pDst = &m_abyRawBlockData[0];
1127 0 : const GByte *pSrc = m_abyDecodedBlockData.data();
1128 0 : for (size_t i = 0; i < nValues;
1129 0 : i++, pDst += nSourceSize, pSrc += nDTSize)
1130 : {
1131 0 : EncodeElt(m_aoDtypeElts, pSrc, pDst);
1132 : }
1133 : }
1134 :
1135 : // 2. Compute shard indices and inner block position within shard
1136 216 : std::vector<uint64_t> anShardIndices(nDims);
1137 216 : std::vector<size_t> anPosInShard(nDims);
1138 348 : for (size_t i = 0; i < nDims; ++i)
1139 : {
1140 480 : anShardIndices[i] = m_anCachedBlockIndices[i] * m_anInnerBlockSize[i] /
1141 240 : m_anOuterBlockSize[i];
1142 480 : anPosInShard[i] = static_cast<size_t>(m_anCachedBlockIndices[i] %
1143 240 : m_anCountInnerBlockInOuter[i]);
1144 : }
1145 :
1146 216 : std::string osFilename = BuildChunkFilename(anShardIndices.data());
1147 :
1148 : // 3. Get or create shard cache entry
1149 108 : size_t nShardElements = 1;
1150 348 : for (size_t i = 0; i < nDims; ++i)
1151 240 : nShardElements *= static_cast<size_t>(m_anOuterBlockSize[i]);
1152 :
1153 108 : size_t nTotalInnerChunks = 1;
1154 348 : for (size_t i = 0; i < nDims; ++i)
1155 240 : nTotalInnerChunks *= static_cast<size_t>(m_anCountInnerBlockInOuter[i]);
1156 :
1157 108 : auto oIt = m_oShardWriteCache.find(osFilename);
1158 108 : if (oIt == m_oShardWriteCache.end())
1159 : {
1160 37 : ShardWriteEntry entry;
1161 : try
1162 : {
1163 37 : entry.abyShardBuffer.resize(nShardElements * nSourceSize);
1164 : }
1165 0 : catch (const std::exception &)
1166 : {
1167 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
1168 : "Cannot allocate memory for shard buffer");
1169 0 : return false;
1170 : }
1171 : try
1172 : {
1173 37 : entry.abDirtyInnerChunks.resize(nTotalInnerChunks, false);
1174 : }
1175 0 : catch (const std::exception &)
1176 : {
1177 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
1178 : "Cannot allocate memory for dirty chunk tracking");
1179 0 : return false;
1180 : }
1181 :
1182 : // Read existing shard or fill with nodata
1183 : VSIStatBufL sStat;
1184 37 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
1185 : {
1186 1 : VSILFILE *fpRead = VSIFOpenL(osFilename.c_str(), "rb");
1187 1 : if (fpRead == nullptr)
1188 : {
1189 0 : CPLError(CE_Failure, CPLE_AppDefined,
1190 : "Cannot open shard file %s for reading",
1191 : osFilename.c_str());
1192 0 : return false;
1193 : }
1194 :
1195 1 : ZarrByteVectorQuickResize abyFileData;
1196 : try
1197 : {
1198 1 : abyFileData.resize(static_cast<size_t>(sStat.st_size));
1199 : }
1200 0 : catch (const std::exception &)
1201 : {
1202 0 : CPLError(
1203 : CE_Failure, CPLE_OutOfMemory,
1204 : "Cannot allocate " CPL_FRMT_GUIB " bytes for shard file %s",
1205 0 : static_cast<GUIntBig>(sStat.st_size), osFilename.c_str());
1206 0 : VSIFCloseL(fpRead);
1207 0 : return false;
1208 : }
1209 2 : if (VSIFReadL(abyFileData.data(), 1, abyFileData.size(), fpRead) !=
1210 1 : abyFileData.size())
1211 : {
1212 0 : CPLError(CE_Failure, CPLE_AppDefined,
1213 : "Cannot read shard file %s", osFilename.c_str());
1214 0 : VSIFCloseL(fpRead);
1215 0 : return false;
1216 : }
1217 1 : VSIFCloseL(fpRead);
1218 :
1219 1 : entry.abyShardBuffer = std::move(abyFileData);
1220 1 : if (!m_poCodecs->Decode(entry.abyShardBuffer))
1221 : {
1222 0 : CPLError(CE_Failure, CPLE_AppDefined,
1223 : "Cannot decode existing shard %s", osFilename.c_str());
1224 0 : return false;
1225 : }
1226 :
1227 1 : if (entry.abyShardBuffer.size() != nShardElements * nSourceSize)
1228 : {
1229 0 : CPLError(CE_Failure, CPLE_AppDefined,
1230 : "Decoded shard %s has unexpected size",
1231 : osFilename.c_str());
1232 0 : return false;
1233 : }
1234 : }
1235 : else
1236 : {
1237 42 : if (m_pabyNoData == nullptr ||
1238 3 : (m_oType.GetClass() == GEDTC_NUMERIC &&
1239 3 : GetNoDataValueAsDouble() == 0.0))
1240 : {
1241 33 : memset(entry.abyShardBuffer.data(), 0,
1242 : entry.abyShardBuffer.size());
1243 : }
1244 : else
1245 : {
1246 4339 : for (size_t i = 0; i < nShardElements; ++i)
1247 : {
1248 4336 : memcpy(entry.abyShardBuffer.data() + i * nSourceSize,
1249 4336 : m_pabyNoData, nSourceSize);
1250 : }
1251 : }
1252 : }
1253 :
1254 37 : oIt = m_oShardWriteCache.emplace(osFilename, std::move(entry)).first;
1255 : }
1256 :
1257 : // cppcheck-suppress derefInvalidIteratorRedundantCheck
1258 108 : auto &entry = oIt->second;
1259 :
1260 : // 4. Compute inner chunk linear index and mark dirty
1261 108 : size_t nInnerChunkIdx = 0;
1262 348 : for (size_t i = 0; i < nDims; ++i)
1263 : {
1264 480 : nInnerChunkIdx = nInnerChunkIdx * static_cast<size_t>(
1265 480 : m_anCountInnerBlockInOuter[i]) +
1266 240 : anPosInShard[i];
1267 : }
1268 108 : entry.abDirtyInnerChunks[nInnerChunkIdx] = true;
1269 : const bool bAllDirty =
1270 108 : std::all_of(entry.abDirtyInnerChunks.begin(),
1271 341 : entry.abDirtyInnerChunks.end(), [](bool b) { return b; });
1272 :
1273 : // 5. Copy dirty inner block into shard buffer at correct position.
1274 : // Same strided N-D copy pattern as CopySubArrayIntoLargerOne() in
1275 : // zarr_v3_codec_sharding.cpp (operates on GUInt64 block sizes here).
1276 : {
1277 216 : std::vector<size_t> anShardStride(nDims);
1278 108 : size_t nStride = nSourceSize;
1279 348 : for (size_t iDim = nDims; iDim > 0;)
1280 : {
1281 240 : --iDim;
1282 240 : anShardStride[iDim] = nStride;
1283 240 : nStride *= static_cast<size_t>(m_anOuterBlockSize[iDim]);
1284 : }
1285 :
1286 108 : GByte *pShardDst = entry.abyShardBuffer.data();
1287 348 : for (size_t iDim = 0; iDim < nDims; ++iDim)
1288 : {
1289 240 : pShardDst += anPosInShard[iDim] *
1290 480 : static_cast<size_t>(m_anInnerBlockSize[iDim]) *
1291 240 : anShardStride[iDim];
1292 : }
1293 :
1294 108 : const GByte *pInnerSrc = m_abyRawBlockData.data();
1295 : const size_t nLastDimBytes =
1296 108 : static_cast<size_t>(m_anInnerBlockSize.back()) * nSourceSize;
1297 :
1298 108 : if (nDims == 1)
1299 : {
1300 0 : memcpy(pShardDst, pInnerSrc, nLastDimBytes);
1301 : }
1302 : else
1303 : {
1304 216 : std::vector<GByte *> dstPtrStack(nDims + 1);
1305 216 : std::vector<size_t> count(nDims + 1);
1306 108 : dstPtrStack[0] = pShardDst;
1307 108 : size_t dimIdx = 0;
1308 755 : lbl_next_depth:
1309 755 : if (dimIdx + 1 == nDims)
1310 : {
1311 623 : memcpy(dstPtrStack[dimIdx], pInnerSrc, nLastDimBytes);
1312 623 : pInnerSrc += nLastDimBytes;
1313 : }
1314 : else
1315 : {
1316 132 : count[dimIdx] = static_cast<size_t>(m_anInnerBlockSize[dimIdx]);
1317 : while (true)
1318 : {
1319 647 : dimIdx++;
1320 647 : dstPtrStack[dimIdx] = dstPtrStack[dimIdx - 1];
1321 647 : goto lbl_next_depth;
1322 647 : lbl_return_to_caller:
1323 647 : dimIdx--;
1324 647 : if (--count[dimIdx] == 0)
1325 132 : break;
1326 515 : dstPtrStack[dimIdx] += anShardStride[dimIdx];
1327 : }
1328 : }
1329 755 : if (dimIdx > 0)
1330 647 : goto lbl_return_to_caller;
1331 : }
1332 : }
1333 :
1334 : // 6. Flush shard immediately if all inner chunks have been written,
1335 : // to bound memory usage during sequential writes.
1336 108 : if (bAllDirty)
1337 : {
1338 18 : const bool bOK = FlushSingleShard(osFilename, entry);
1339 18 : m_oShardWriteCache.erase(osFilename);
1340 18 : return bOK;
1341 : }
1342 :
1343 90 : return true;
1344 : }
1345 :
1346 : /************************************************************************/
1347 : /* ZarrV3Array::FlushSingleShard() */
1348 : /************************************************************************/
1349 :
1350 37 : bool ZarrV3Array::FlushSingleShard(const std::string &osFilename,
1351 : ShardWriteEntry &entry) const
1352 : {
1353 : // Encode mutates abyShardBuffer in-place. On failure the buffer
1354 : // is left in an undefined state, but the shard is not written.
1355 37 : if (!m_poCodecs->Encode(entry.abyShardBuffer))
1356 : {
1357 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot encode shard for %s",
1358 : osFilename.c_str());
1359 0 : return false;
1360 : }
1361 :
1362 : // All-nodata shard: skip writing (or delete stale file from prior write)
1363 37 : if (entry.abyShardBuffer.empty())
1364 : {
1365 : VSIStatBufL sStat;
1366 0 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
1367 0 : VSIUnlink(osFilename.c_str());
1368 0 : return true;
1369 : }
1370 :
1371 : // Create directory if needed
1372 37 : if (m_osDimSeparator == "/")
1373 : {
1374 37 : std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
1375 : VSIStatBufL sStatDir;
1376 37 : if (VSIStatL(osDir.c_str(), &sStatDir) != 0)
1377 : {
1378 16 : if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
1379 : {
1380 0 : CPLError(CE_Failure, CPLE_AppDefined,
1381 : "Cannot create directory %s", osDir.c_str());
1382 0 : return false;
1383 : }
1384 : }
1385 : }
1386 :
1387 37 : VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
1388 37 : if (fp == nullptr)
1389 : {
1390 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot create shard file %s",
1391 : osFilename.c_str());
1392 0 : return false;
1393 : }
1394 :
1395 37 : const size_t nEncodedSize = entry.abyShardBuffer.size();
1396 37 : bool bRet = true;
1397 37 : if (VSIFWriteL(entry.abyShardBuffer.data(), 1, nEncodedSize, fp) !=
1398 : nEncodedSize)
1399 : {
1400 0 : CPLError(CE_Failure, CPLE_AppDefined,
1401 : "Could not write shard file %s correctly", osFilename.c_str());
1402 0 : bRet = false;
1403 : }
1404 37 : VSIFCloseL(fp);
1405 37 : if (bRet)
1406 37 : ZarrEraseShardIndexFromCache(osFilename);
1407 37 : return bRet;
1408 : }
1409 :
1410 : /************************************************************************/
1411 : /* ZarrV3Array::FlushShardCache() */
1412 : /************************************************************************/
1413 :
1414 : // Encodes and writes all cached shards. Called from Flush().
1415 : // Each shard is encoded exactly once regardless of how many inner chunks
1416 : // were written.
1417 :
1418 5886 : bool ZarrV3Array::FlushShardCache() const
1419 : {
1420 5886 : if (m_oShardWriteCache.empty())
1421 5879 : return true;
1422 :
1423 7 : bool bRet = true;
1424 26 : for (auto &[osFilename, entry] : m_oShardWriteCache)
1425 : {
1426 19 : if (!FlushSingleShard(osFilename, entry))
1427 0 : bRet = false;
1428 : }
1429 :
1430 7 : m_oShardWriteCache.clear();
1431 7 : return bRet;
1432 : }
1433 :
1434 : /************************************************************************/
1435 : /* ExtractSubArray() */
1436 : /************************************************************************/
1437 :
1438 4172 : static void ExtractSubArray(const GByte *const pabySrc,
1439 : const std::vector<size_t> &anSrcStart,
1440 : const std::vector<GPtrDiff_t> &anSrcStrideElts,
1441 : const std::vector<size_t> &anCount,
1442 : GByte *const pabyDst,
1443 : const std::vector<GPtrDiff_t> &anDstStrideElts,
1444 : const size_t nDTSize)
1445 : {
1446 4172 : const auto nDims = anSrcStart.size();
1447 4172 : CPLAssert(nDims > 0);
1448 4172 : CPLAssert(nDims == anSrcStrideElts.size());
1449 4172 : CPLAssert(nDims == anCount.size());
1450 4172 : CPLAssert(nDims == anDstStrideElts.size());
1451 :
1452 : #if defined(__GNUC__)
1453 : #pragma GCC diagnostic push
1454 : #pragma GCC diagnostic ignored "-Wnull-dereference"
1455 : #endif
1456 8344 : std::vector<const GByte *> srcPtrStack(nDims);
1457 8344 : std::vector<GByte *> dstPtrStack(nDims);
1458 8344 : std::vector<GPtrDiff_t> anSrcStrideBytes(nDims);
1459 8344 : std::vector<GPtrDiff_t> anDstStrideBytes(nDims);
1460 8344 : std::vector<size_t> count(nDims);
1461 :
1462 4172 : srcPtrStack[0] = pabySrc;
1463 16688 : for (size_t i = 0; i < nDims; ++i)
1464 : {
1465 12516 : anSrcStrideBytes[i] = anSrcStrideElts[i] * nDTSize;
1466 12516 : anDstStrideBytes[i] = anDstStrideElts[i] * nDTSize;
1467 12516 : srcPtrStack[0] += anSrcStart[i] * anSrcStrideBytes[i];
1468 : }
1469 4172 : dstPtrStack[0] = pabyDst;
1470 : #if defined(__GNUC__)
1471 : #pragma GCC diagnostic pop
1472 : #endif
1473 :
1474 4172 : const size_t nLastDimSize = anCount.back() * nDTSize;
1475 4172 : size_t dimIdx = 0;
1476 357989 : lbl_next_depth:
1477 357989 : if (dimIdx + 1 == nDims)
1478 : {
1479 341847 : memcpy(dstPtrStack[dimIdx], srcPtrStack[dimIdx], nLastDimSize);
1480 : }
1481 : else
1482 : {
1483 16142 : count[dimIdx] = anCount[dimIdx];
1484 : while (true)
1485 : {
1486 353817 : dimIdx++;
1487 353817 : srcPtrStack[dimIdx] = srcPtrStack[dimIdx - 1];
1488 353817 : dstPtrStack[dimIdx] = dstPtrStack[dimIdx - 1];
1489 353817 : goto lbl_next_depth;
1490 353817 : lbl_return_to_caller:
1491 353817 : dimIdx--;
1492 353817 : if (--count[dimIdx] == 0)
1493 16142 : break;
1494 337675 : srcPtrStack[dimIdx] += anSrcStrideBytes[dimIdx];
1495 337675 : dstPtrStack[dimIdx] += anDstStrideBytes[dimIdx];
1496 : }
1497 : }
1498 357989 : if (dimIdx > 0)
1499 353817 : goto lbl_return_to_caller;
1500 4172 : }
1501 :
1502 : /************************************************************************/
1503 : /* ZarrV3Array::WriteChunksThreadSafe() */
1504 : /************************************************************************/
1505 :
1506 36 : bool ZarrV3Array::WriteChunksThreadSafe(
1507 : const GUInt64 *arrayStartIdx, const size_t *count,
1508 : [[maybe_unused]] const GInt64 *arrayStep, const GPtrDiff_t *bufferStride,
1509 : [[maybe_unused]] const GDALExtendedDataType &bufferDataType,
1510 : const void *pSrcBuffer, const int iThread, const int nThreads,
1511 : std::string &osErrorMsg) const
1512 : {
1513 36 : CPLAssert(m_oType == bufferDataType);
1514 :
1515 36 : const auto nDims = GetDimensionCount();
1516 72 : std::vector<size_t> anChunkCount(nDims);
1517 72 : std::vector<size_t> anChunkCoord(nDims);
1518 36 : size_t nChunks = 1;
1519 144 : for (size_t i = 0; i < nDims; ++i)
1520 : {
1521 108 : CPLAssert(count[i] == 1 || arrayStep[i] == 1);
1522 108 : anChunkCount[i] = static_cast<size_t>(cpl::div_round_up(
1523 108 : static_cast<uint64_t>(count[i]), m_anOuterBlockSize[i]));
1524 108 : nChunks *= anChunkCount[i];
1525 : }
1526 :
1527 36 : const size_t iFirstChunk = static_cast<size_t>(
1528 36 : (static_cast<uint64_t>(iThread) * nChunks) / nThreads);
1529 36 : const size_t iLastChunkExcluded = static_cast<size_t>(
1530 36 : (static_cast<uint64_t>(iThread + 1) * nChunks) / nThreads);
1531 :
1532 72 : std::vector<size_t> anSrcStart(nDims);
1533 : const std::vector<GPtrDiff_t> anSrcStrideElts(bufferStride,
1534 72 : bufferStride + nDims);
1535 72 : std::vector<GPtrDiff_t> anDstStrideElts(nDims);
1536 :
1537 36 : size_t nDstStride = 1;
1538 144 : for (size_t i = nDims, iChunkCur = iFirstChunk; i > 0;)
1539 : {
1540 108 : --i;
1541 108 : anChunkCoord[i] = iChunkCur % anChunkCount[i];
1542 108 : iChunkCur /= anChunkCount[i];
1543 :
1544 108 : anDstStrideElts[i] = nDstStride;
1545 108 : nDstStride *= static_cast<size_t>(m_anOuterBlockSize[i]);
1546 : }
1547 :
1548 3 : const auto StoreError = [this, &osErrorMsg](const std::string &s)
1549 : {
1550 1 : std::lock_guard oLock(m_oMutex);
1551 1 : if (!osErrorMsg.empty())
1552 0 : osErrorMsg += '\n';
1553 1 : osErrorMsg = s;
1554 2 : return false;
1555 36 : };
1556 :
1557 36 : const size_t nDTSize = m_oType.GetSize();
1558 : const size_t nDstSize =
1559 36 : static_cast<size_t>(MultiplyElements(m_anOuterBlockSize)) * nDTSize;
1560 72 : ZarrByteVectorQuickResize abyDst;
1561 : try
1562 : {
1563 36 : abyDst.resize(nDstSize);
1564 : }
1565 0 : catch (const std::exception &)
1566 : {
1567 0 : return StoreError("Out of memory allocating temporary buffer");
1568 : }
1569 :
1570 36 : std::unique_ptr<ZarrV3CodecSequence> poCodecs;
1571 36 : if (m_poCodecs)
1572 : {
1573 : // Codec cloning is not thread safe
1574 36 : std::lock_guard oLock(m_oMutex);
1575 36 : poCodecs = m_poCodecs->Clone();
1576 : }
1577 :
1578 72 : std::vector<uint64_t> anChunkIndex(nDims);
1579 72 : std::vector<size_t> anCount(nDims);
1580 4207 : for (size_t iChunk = iFirstChunk; iChunk < iLastChunkExcluded; ++iChunk)
1581 : {
1582 4172 : if (iChunk > iFirstChunk)
1583 : {
1584 4136 : size_t iDimToIncrement = nDims - 1;
1585 8346 : while (++anChunkCoord[iDimToIncrement] ==
1586 4173 : anChunkCount[iDimToIncrement])
1587 : {
1588 37 : anChunkCoord[iDimToIncrement] = 0;
1589 37 : CPLAssert(iDimToIncrement >= 1);
1590 37 : --iDimToIncrement;
1591 : }
1592 : }
1593 :
1594 4172 : bool bPartialChunk = false;
1595 16688 : for (size_t i = 0; i < nDims; ++i)
1596 : {
1597 25032 : anChunkIndex[i] =
1598 12516 : anChunkCoord[i] + arrayStartIdx[i] / m_anOuterBlockSize[i];
1599 25032 : anSrcStart[i] =
1600 12516 : anChunkCoord[i] * static_cast<size_t>(m_anOuterBlockSize[i]);
1601 12516 : anCount[i] = static_cast<size_t>(std::min(
1602 12516 : m_aoDims[i]->GetSize() - arrayStartIdx[i] - anSrcStart[i],
1603 25032 : m_anOuterBlockSize[i]));
1604 12516 : bPartialChunk = bPartialChunk || anCount[i] < m_anOuterBlockSize[i];
1605 : }
1606 :
1607 : // Resize to target size, as a previous iteration may have shorten it
1608 : // during compression.
1609 4172 : abyDst.resize(nDstSize);
1610 4172 : if (bPartialChunk)
1611 3832 : memset(abyDst.data(), 0, nDstSize);
1612 :
1613 4172 : ExtractSubArray(static_cast<const GByte *>(pSrcBuffer), anSrcStart,
1614 : anSrcStrideElts, anCount, abyDst.data(),
1615 : anDstStrideElts, nDTSize);
1616 :
1617 4172 : const std::string osFilename = BuildChunkFilename(anChunkIndex.data());
1618 4172 : if (IsEmptyBlock(abyDst))
1619 : {
1620 : VSIStatBufL sStat;
1621 0 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
1622 : {
1623 0 : CPLDebugOnly(ZARR_DEBUG_KEY,
1624 : "Deleting chunk %s that has now empty content",
1625 : osFilename.c_str());
1626 0 : if (VSIUnlink(osFilename.c_str()) != 0)
1627 : {
1628 0 : return StoreError("Chunk " + osFilename +
1629 0 : " deletion failed");
1630 : }
1631 : }
1632 0 : continue;
1633 : }
1634 :
1635 4172 : if (poCodecs)
1636 : {
1637 4172 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
1638 4172 : if (!poCodecs->Encode(abyDst))
1639 : {
1640 0 : return StoreError(CPLGetLastErrorMsg());
1641 : }
1642 : }
1643 :
1644 4172 : if (m_osDimSeparator == "/")
1645 : {
1646 4172 : const std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
1647 : VSIStatBufL sStat;
1648 4221 : if (VSIStatL(osDir.c_str(), &sStat) != 0 &&
1649 49 : VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
1650 : {
1651 0 : return StoreError("Cannot create directory " + osDir);
1652 : }
1653 : }
1654 :
1655 4172 : auto fp = VSIFilesystemHandler::OpenStatic(osFilename.c_str(), "wb");
1656 4172 : if (fp == nullptr)
1657 : {
1658 1 : return StoreError("Cannot create file " + osFilename);
1659 : }
1660 :
1661 8342 : if (fp->Write(abyDst.data(), abyDst.size()) != abyDst.size() ||
1662 4171 : fp->Close() != 0)
1663 : {
1664 0 : return StoreError("Write error while writing " + osFilename);
1665 : }
1666 : }
1667 :
1668 35 : return true;
1669 : }
1670 :
1671 : /************************************************************************/
1672 : /* ZarrV3Array::IWrite() */
1673 : /************************************************************************/
1674 :
1675 146 : bool ZarrV3Array::IWrite(const GUInt64 *arrayStartIdx, const size_t *count,
1676 : const GInt64 *arrayStep,
1677 : const GPtrDiff_t *bufferStride,
1678 : const GDALExtendedDataType &bufferDataType,
1679 : const void *pSrcBuffer)
1680 : {
1681 :
1682 : // Multithreading writing if window is aligned on chunk boundaries.
1683 146 : if (m_oType == bufferDataType && m_oType.GetClass() == GEDTC_NUMERIC)
1684 : {
1685 135 : const auto nDims = GetDimensionCount();
1686 135 : bool bCanUseMultiThreading = true;
1687 135 : size_t nChunks = 1;
1688 396 : for (size_t i = 0; i < nDims; ++i)
1689 : {
1690 269 : if ((arrayStartIdx[i] % m_anOuterBlockSize[i]) != 0 ||
1691 535 : (count[i] != 1 && arrayStep[i] != 1) ||
1692 301 : !((count[i] % m_anOuterBlockSize[i]) == 0 ||
1693 35 : arrayStartIdx[i] + count[i] == m_aoDims[i]->GetSize()))
1694 : {
1695 8 : bCanUseMultiThreading = false;
1696 8 : break;
1697 : }
1698 261 : nChunks *= static_cast<size_t>(cpl::div_round_up(
1699 261 : static_cast<uint64_t>(count[i]), m_anOuterBlockSize[i]));
1700 : }
1701 135 : if (bCanUseMultiThreading && nChunks >= 2)
1702 : {
1703 : const int nMaxThreads = static_cast<int>(
1704 30 : std::min<size_t>(nChunks, GDAL_DEFAULT_MAX_THREAD_COUNT));
1705 : const int nThreads =
1706 30 : GDALGetNumThreads(nMaxThreads, /* bDefaultAllCPUs=*/false);
1707 : CPLWorkerThreadPool *wtp =
1708 30 : nThreads >= 2 ? GDALGetGlobalThreadPool(nThreads) : nullptr;
1709 :
1710 30 : if (wtp)
1711 : {
1712 9 : m_oChunkCache.clear();
1713 :
1714 9 : if (!FlushDirtyBlock())
1715 0 : return false;
1716 :
1717 9 : CPLDebug("Zarr", "Using %d threads for writing", nThreads);
1718 18 : auto poJobQueue = wtp->CreateJobQueue();
1719 9 : std::atomic<bool> bSuccess = true;
1720 18 : std::string osErrorMsg;
1721 45 : for (int iThread = 0; iThread < nThreads; ++iThread)
1722 : {
1723 36 : auto job = [this, iThread, nThreads, arrayStartIdx, count,
1724 : arrayStep, bufferStride, pSrcBuffer,
1725 73 : &bufferDataType, &bSuccess, &osErrorMsg]()
1726 : {
1727 72 : if (bSuccess &&
1728 36 : !WriteChunksThreadSafe(
1729 : arrayStartIdx, count, arrayStep, bufferStride,
1730 : bufferDataType, pSrcBuffer, iThread, nThreads,
1731 36 : osErrorMsg))
1732 : {
1733 1 : bSuccess = false;
1734 : }
1735 72 : };
1736 36 : if (!poJobQueue->SubmitJob(job))
1737 : {
1738 0 : CPLError(
1739 : CE_Failure, CPLE_AppDefined,
1740 : "ZarrV3Array::IWrite(): job submission failed");
1741 0 : return false;
1742 : }
1743 : }
1744 :
1745 9 : poJobQueue->WaitCompletion();
1746 :
1747 9 : if (!bSuccess)
1748 : {
1749 1 : CPLError(CE_Failure, CPLE_AppDefined,
1750 : "ZarrV3Array::IWrite(): %s", osErrorMsg.c_str());
1751 : }
1752 :
1753 9 : return bSuccess;
1754 : }
1755 : }
1756 : }
1757 :
1758 137 : return ZarrArray::IWrite(arrayStartIdx, count, arrayStep, bufferStride,
1759 137 : bufferDataType, pSrcBuffer);
1760 : }
1761 :
1762 : /************************************************************************/
1763 : /* BuildChunkFilename() */
1764 : /************************************************************************/
1765 :
1766 44444 : std::string ZarrV3Array::BuildChunkFilename(const uint64_t *blockIndices) const
1767 : {
1768 44444 : if (m_aoDims.empty())
1769 : {
1770 : return CPLFormFilenameSafe(
1771 0 : CPLGetDirnameSafe(m_osFilename.c_str()).c_str(),
1772 0 : m_bV2ChunkKeyEncoding ? "0" : "c", nullptr);
1773 : }
1774 : else
1775 : {
1776 88888 : std::string osFilename(CPLGetDirnameSafe(m_osFilename.c_str()));
1777 44444 : osFilename += '/';
1778 44444 : if (!m_bV2ChunkKeyEncoding)
1779 : {
1780 44435 : osFilename += 'c';
1781 : }
1782 141773 : for (size_t i = 0; i < m_aoDims.size(); ++i)
1783 : {
1784 97329 : if (i > 0 || !m_bV2ChunkKeyEncoding)
1785 97320 : osFilename += m_osDimSeparator;
1786 97329 : osFilename += std::to_string(blockIndices[i]);
1787 : }
1788 44444 : return osFilename;
1789 : }
1790 : }
1791 :
1792 : /************************************************************************/
1793 : /* GetDataDirectory() */
1794 : /************************************************************************/
1795 :
1796 5 : std::string ZarrV3Array::GetDataDirectory() const
1797 : {
1798 5 : return std::string(CPLGetDirnameSafe(m_osFilename.c_str()));
1799 : }
1800 :
1801 : /************************************************************************/
1802 : /* GetChunkIndicesFromFilename() */
1803 : /************************************************************************/
1804 :
1805 : CPLStringList
1806 19 : ZarrV3Array::GetChunkIndicesFromFilename(const char *pszFilename) const
1807 : {
1808 19 : if (!m_bV2ChunkKeyEncoding)
1809 : {
1810 19 : if (pszFilename[0] != 'c')
1811 6 : return CPLStringList();
1812 13 : if (m_osDimSeparator == "/")
1813 : {
1814 13 : if (pszFilename[1] != '/' && pszFilename[1] != '\\')
1815 0 : return CPLStringList();
1816 : }
1817 0 : else if (pszFilename[1] != m_osDimSeparator[0])
1818 : {
1819 0 : return CPLStringList();
1820 : }
1821 : }
1822 : return CPLStringList(
1823 13 : CSLTokenizeString2(pszFilename + (m_bV2ChunkKeyEncoding ? 0 : 2),
1824 26 : m_osDimSeparator.c_str(), 0));
1825 : }
1826 :
1827 : /************************************************************************/
1828 : /* ParseDtypeV3() */
1829 : /************************************************************************/
1830 :
1831 1167 : static GDALExtendedDataType ParseDtypeV3(const CPLJSONObject &obj,
1832 : std::vector<DtypeElt> &elts)
1833 : {
1834 : do
1835 : {
1836 1167 : if (obj.GetType() == CPLJSONObject::Type::String)
1837 : {
1838 2318 : const auto str = obj.ToString();
1839 1159 : DtypeElt elt;
1840 1159 : GDALDataType eDT = GDT_Unknown;
1841 :
1842 1159 : if (str == "bool") // boolean
1843 : {
1844 0 : elt.nativeType = DtypeElt::NativeType::BOOLEAN;
1845 0 : eDT = GDT_UInt8;
1846 : }
1847 1159 : else if (str == "int8")
1848 : {
1849 6 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
1850 6 : eDT = GDT_Int8;
1851 : }
1852 1153 : else if (str == "uint8")
1853 : {
1854 116 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
1855 116 : eDT = GDT_UInt8;
1856 : }
1857 1037 : else if (str == "int16")
1858 : {
1859 11 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
1860 11 : eDT = GDT_Int16;
1861 : }
1862 1026 : else if (str == "uint16")
1863 : {
1864 9 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
1865 9 : eDT = GDT_UInt16;
1866 : }
1867 1017 : else if (str == "int32")
1868 : {
1869 7 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
1870 7 : eDT = GDT_Int32;
1871 : }
1872 1010 : else if (str == "uint32")
1873 : {
1874 7 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
1875 7 : eDT = GDT_UInt32;
1876 : }
1877 1003 : else if (str == "int64")
1878 : {
1879 146 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
1880 146 : eDT = GDT_Int64;
1881 : }
1882 857 : else if (str == "uint64")
1883 : {
1884 6 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
1885 6 : eDT = GDT_UInt64;
1886 : }
1887 851 : else if (str == "float16")
1888 : {
1889 3 : elt.nativeType = DtypeElt::NativeType::IEEEFP;
1890 3 : eDT = GDT_Float16;
1891 : }
1892 848 : else if (str == "float32")
1893 : {
1894 782 : elt.nativeType = DtypeElt::NativeType::IEEEFP;
1895 782 : eDT = GDT_Float32;
1896 : }
1897 66 : else if (str == "float64")
1898 : {
1899 27 : elt.nativeType = DtypeElt::NativeType::IEEEFP;
1900 27 : eDT = GDT_Float64;
1901 : }
1902 39 : else if (str == "complex64")
1903 : {
1904 15 : elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
1905 15 : eDT = GDT_CFloat32;
1906 : }
1907 24 : else if (str == "complex128")
1908 : {
1909 15 : elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
1910 15 : eDT = GDT_CFloat64;
1911 : }
1912 9 : else if (str == "string")
1913 : {
1914 : // Variable-length UTF-8 strings (zarr-extensions).
1915 : // nativeSize determines the fixed slot size used by the
1916 : // vlen-utf8 codec; strings exceeding (nativeSize-1) are
1917 : // truncated. Increase via ZARR_VLEN_STRING_MAX_LENGTH.
1918 : const int nMaxLen =
1919 8 : std::max(2, atoi(CPLGetConfigOption(
1920 8 : "ZARR_VLEN_STRING_MAX_LENGTH", "256")));
1921 8 : elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
1922 8 : elt.nativeSize = static_cast<size_t>(nMaxLen);
1923 16 : elt.gdalType = GDALExtendedDataType::CreateString(
1924 8 : static_cast<size_t>(nMaxLen));
1925 8 : elt.gdalSize = elt.gdalType.GetSize();
1926 8 : elts.emplace_back(elt);
1927 : return GDALExtendedDataType::CreateString(
1928 8 : static_cast<size_t>(nMaxLen));
1929 : }
1930 : else
1931 1 : break;
1932 :
1933 1150 : elt.gdalType = GDALExtendedDataType::Create(eDT);
1934 1150 : elt.gdalSize = elt.gdalType.GetSize();
1935 1150 : if (!elt.gdalTypeIsApproxOfNative)
1936 1150 : elt.nativeSize = elt.gdalSize;
1937 1150 : if (elt.nativeSize > 1)
1938 : {
1939 1028 : elt.needByteSwapping = (CPL_IS_LSB == 0);
1940 : }
1941 1150 : elts.emplace_back(elt);
1942 1150 : return GDALExtendedDataType::Create(eDT);
1943 : }
1944 8 : else if (obj.GetType() == CPLJSONObject::Type::Object)
1945 : {
1946 16 : const auto osName = obj["name"].ToString();
1947 16 : const auto oConfig = obj["configuration"];
1948 8 : DtypeElt elt;
1949 :
1950 8 : if (osName == "null_terminated_bytes" && oConfig.IsValid())
1951 : {
1952 2 : const int nBytes = oConfig["length_bytes"].ToInteger();
1953 2 : if (nBytes <= 0 || nBytes > 10 * 1024 * 1024)
1954 : break;
1955 2 : elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
1956 2 : elt.nativeSize = static_cast<size_t>(nBytes);
1957 4 : elt.gdalType = GDALExtendedDataType::CreateString(
1958 2 : static_cast<size_t>(nBytes));
1959 2 : elt.gdalSize = elt.gdalType.GetSize();
1960 2 : elts.emplace_back(elt);
1961 : return GDALExtendedDataType::CreateString(
1962 2 : static_cast<size_t>(nBytes));
1963 : }
1964 6 : else if (osName == "fixed_length_utf32" && oConfig.IsValid())
1965 : {
1966 1 : const int nBytes = oConfig["length_bytes"].ToInteger();
1967 1 : if (nBytes <= 0 || nBytes % 4 != 0 || nBytes > 10 * 1024 * 1024)
1968 : break;
1969 1 : elt.nativeType = DtypeElt::NativeType::STRING_UNICODE;
1970 1 : elt.nativeSize = static_cast<size_t>(nBytes);
1971 : // Endianness handled by the bytes codec in v3
1972 1 : elt.gdalType = GDALExtendedDataType::CreateString();
1973 1 : elt.gdalSize = elt.gdalType.GetSize();
1974 1 : elts.emplace_back(elt);
1975 1 : return GDALExtendedDataType::CreateString();
1976 : }
1977 8 : else if (osName == "numpy.datetime64" ||
1978 3 : osName == "numpy.timedelta64")
1979 : {
1980 4 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
1981 4 : elt.gdalType = GDALExtendedDataType::Create(GDT_Int64);
1982 4 : elt.gdalSize = elt.gdalType.GetSize();
1983 4 : elt.nativeSize = elt.gdalSize;
1984 4 : elt.needByteSwapping = (CPL_IS_LSB == 0);
1985 4 : elts.emplace_back(elt);
1986 4 : return GDALExtendedDataType::Create(GDT_Int64);
1987 : }
1988 : }
1989 : } while (false);
1990 2 : CPLError(CE_Failure, CPLE_AppDefined,
1991 : "Invalid or unsupported format for data_type: %s",
1992 4 : obj.ToString().c_str());
1993 2 : return GDALExtendedDataType::Create(GDT_Unknown);
1994 : }
1995 :
1996 : /************************************************************************/
1997 : /* ParseNoDataStringAsDouble() */
1998 : /************************************************************************/
1999 :
2000 297 : static double ParseNoDataStringAsDouble(const std::string &osVal, bool &bOK)
2001 : {
2002 297 : double dfNoDataValue = std::numeric_limits<double>::quiet_NaN();
2003 297 : if (osVal == "NaN")
2004 : {
2005 : // initialized above
2006 : }
2007 15 : else if (osVal == "Infinity" || osVal == "+Infinity")
2008 : {
2009 5 : dfNoDataValue = std::numeric_limits<double>::infinity();
2010 : }
2011 10 : else if (osVal == "-Infinity")
2012 : {
2013 5 : dfNoDataValue = -std::numeric_limits<double>::infinity();
2014 : }
2015 : else
2016 : {
2017 5 : bOK = false;
2018 : }
2019 297 : return dfNoDataValue;
2020 : }
2021 :
2022 : /************************************************************************/
2023 : /* ParseNoDataComponent() */
2024 : /************************************************************************/
2025 :
2026 : template <typename T, typename Tint>
2027 40 : static T ParseNoDataComponent(const CPLJSONObject &oObj, bool &bOK)
2028 : {
2029 40 : if (oObj.GetType() == CPLJSONObject::Type::Integer ||
2030 62 : oObj.GetType() == CPLJSONObject::Type::Long ||
2031 22 : oObj.GetType() == CPLJSONObject::Type::Double)
2032 : {
2033 22 : return static_cast<T>(oObj.ToDouble());
2034 : }
2035 18 : else if (oObj.GetType() == CPLJSONObject::Type::String)
2036 : {
2037 54 : const auto osVal = oObj.ToString();
2038 18 : if (STARTS_WITH(osVal.c_str(), "0x"))
2039 : {
2040 2 : if (osVal.size() > 2 + 2 * sizeof(T))
2041 : {
2042 0 : bOK = false;
2043 0 : return 0;
2044 : }
2045 2 : Tint nVal = static_cast<Tint>(
2046 2 : std::strtoull(osVal.c_str() + 2, nullptr, 16));
2047 : T fVal;
2048 : static_assert(sizeof(nVal) == sizeof(fVal),
2049 : "sizeof(nVal) == sizeof(dfVal)");
2050 2 : memcpy(&fVal, &nVal, sizeof(nVal));
2051 2 : return fVal;
2052 : }
2053 : else
2054 : {
2055 16 : return static_cast<T>(ParseNoDataStringAsDouble(osVal, bOK));
2056 : }
2057 : }
2058 : else
2059 : {
2060 0 : bOK = false;
2061 0 : return 0;
2062 : }
2063 : }
2064 :
2065 : /************************************************************************/
2066 : /* ZarrV3Group::LoadArray() */
2067 : /************************************************************************/
2068 :
2069 : std::shared_ptr<ZarrArray>
2070 1180 : ZarrV3Group::LoadArray(const std::string &osArrayName,
2071 : const std::string &osZarrayFilename,
2072 : const CPLJSONObject &oRoot) const
2073 : {
2074 : // Add osZarrayFilename to m_poSharedResource during the scope
2075 : // of this function call.
2076 1180 : ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource,
2077 2360 : osZarrayFilename);
2078 1180 : if (!filenameAdder.ok())
2079 0 : return nullptr;
2080 :
2081 : // Warn about unknown members (the spec suggests to error out, but let be
2082 : // a bit more lenient)
2083 12940 : for (const auto &oNode : oRoot.GetChildren())
2084 : {
2085 23520 : const auto osName = oNode.GetName();
2086 31740 : if (osName != "zarr_format" && osName != "node_type" &&
2087 24663 : osName != "shape" && osName != "chunk_grid" &&
2088 17589 : osName != "data_type" && osName != "chunk_key_encoding" &&
2089 8188 : osName != "fill_value" &&
2090 : // Below are optional
2091 8578 : osName != "dimension_names" && osName != "codecs" &&
2092 24318 : osName != "storage_transformers" && osName != "attributes")
2093 : {
2094 4 : CPLError(CE_Warning, CPLE_AppDefined,
2095 : "%s array definition contains a unknown member (%s). "
2096 : "Interpretation of the array might be wrong.",
2097 : osZarrayFilename.c_str(), osName.c_str());
2098 : }
2099 : }
2100 :
2101 3540 : const auto oStorageTransformers = oRoot["storage_transformers"].ToArray();
2102 1180 : if (oStorageTransformers.Size() > 0)
2103 : {
2104 1 : CPLError(CE_Failure, CPLE_AppDefined,
2105 : "storage_transformers are not supported.");
2106 1 : return nullptr;
2107 : }
2108 :
2109 3537 : const auto oShape = oRoot["shape"].ToArray();
2110 1179 : if (!oShape.IsValid())
2111 : {
2112 2 : CPLError(CE_Failure, CPLE_AppDefined, "shape missing or not an array");
2113 2 : return nullptr;
2114 : }
2115 :
2116 : // Parse chunk_grid
2117 3531 : const auto oChunkGrid = oRoot["chunk_grid"];
2118 1177 : if (oChunkGrid.GetType() != CPLJSONObject::Type::Object)
2119 : {
2120 1 : CPLError(CE_Failure, CPLE_AppDefined,
2121 : "chunk_grid missing or not an object");
2122 1 : return nullptr;
2123 : }
2124 :
2125 3528 : const auto oChunkGridName = oChunkGrid["name"];
2126 1176 : if (oChunkGridName.ToString() != "regular")
2127 : {
2128 1 : CPLError(CE_Failure, CPLE_AppDefined,
2129 : "Only chunk_grid.name = regular supported");
2130 1 : return nullptr;
2131 : }
2132 :
2133 3525 : const auto oChunks = oChunkGrid["configuration"]["chunk_shape"].ToArray();
2134 1175 : if (!oChunks.IsValid())
2135 : {
2136 1 : CPLError(
2137 : CE_Failure, CPLE_AppDefined,
2138 : "chunk_grid.configuration.chunk_shape missing or not an array");
2139 1 : return nullptr;
2140 : }
2141 :
2142 1174 : if (oShape.Size() != oChunks.Size())
2143 : {
2144 1 : CPLError(CE_Failure, CPLE_AppDefined,
2145 : "shape and chunks arrays are of different size");
2146 1 : return nullptr;
2147 : }
2148 :
2149 : // Parse chunk_key_encoding
2150 3519 : const auto oChunkKeyEncoding = oRoot["chunk_key_encoding"];
2151 1173 : if (oChunkKeyEncoding.GetType() != CPLJSONObject::Type::Object)
2152 : {
2153 1 : CPLError(CE_Failure, CPLE_AppDefined,
2154 : "chunk_key_encoding missing or not an object");
2155 1 : return nullptr;
2156 : }
2157 :
2158 2344 : std::string osDimSeparator;
2159 1172 : bool bV2ChunkKeyEncoding = false;
2160 3516 : const auto oChunkKeyEncodingName = oChunkKeyEncoding["name"];
2161 1172 : if (oChunkKeyEncodingName.ToString() == "default")
2162 : {
2163 1159 : osDimSeparator = "/";
2164 : }
2165 13 : else if (oChunkKeyEncodingName.ToString() == "v2")
2166 : {
2167 12 : osDimSeparator = ".";
2168 12 : bV2ChunkKeyEncoding = true;
2169 : }
2170 : else
2171 : {
2172 1 : CPLError(CE_Failure, CPLE_AppDefined,
2173 : "Unsupported chunk_key_encoding.name");
2174 1 : return nullptr;
2175 : }
2176 :
2177 : {
2178 2342 : auto oConfiguration = oChunkKeyEncoding["configuration"];
2179 1171 : if (oConfiguration.GetType() == CPLJSONObject::Type::Object)
2180 : {
2181 2222 : auto oSeparator = oConfiguration["separator"];
2182 1111 : if (oSeparator.IsValid())
2183 : {
2184 1111 : osDimSeparator = oSeparator.ToString();
2185 1111 : if (osDimSeparator != "/" && osDimSeparator != ".")
2186 : {
2187 1 : CPLError(CE_Failure, CPLE_AppDefined,
2188 : "Separator can only be '/' or '.'");
2189 1 : return nullptr;
2190 : }
2191 : }
2192 : }
2193 : }
2194 :
2195 3510 : CPLJSONObject oAttributes = oRoot["attributes"];
2196 :
2197 : // Deep-clone of oAttributes
2198 1170 : if (oAttributes.IsValid())
2199 : {
2200 1092 : oAttributes = oAttributes.Clone();
2201 : }
2202 :
2203 2340 : std::vector<std::shared_ptr<GDALDimension>> aoDims;
2204 3316 : for (int i = 0; i < oShape.Size(); ++i)
2205 : {
2206 2146 : const auto nSize = static_cast<GUInt64>(oShape[i].ToLong());
2207 2146 : if (nSize == 0)
2208 : {
2209 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid content for shape");
2210 0 : return nullptr;
2211 : }
2212 2146 : aoDims.emplace_back(std::make_shared<ZarrDimension>(
2213 2146 : m_poSharedResource,
2214 4292 : std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
2215 4292 : std::string(), CPLSPrintf("dim%d", i), std::string(), std::string(),
2216 2146 : nSize));
2217 : }
2218 :
2219 : // Deal with dimension_names
2220 3510 : const auto dimensionNames = oRoot["dimension_names"];
2221 :
2222 722 : const auto FindDimension = [this, &aoDims, &osArrayName, &oAttributes](
2223 : const std::string &osDimName,
2224 7778 : std::shared_ptr<GDALDimension> &poDim, int i)
2225 : {
2226 722 : auto oIter = m_oMapDimensions.find(osDimName);
2227 722 : if (oIter != m_oMapDimensions.end())
2228 : {
2229 308 : if (m_bDimSizeInUpdate ||
2230 154 : oIter->second->GetSize() == poDim->GetSize())
2231 : {
2232 154 : poDim = oIter->second;
2233 154 : return true;
2234 : }
2235 : else
2236 : {
2237 0 : CPLError(CE_Warning, CPLE_AppDefined,
2238 : "Size of _ARRAY_DIMENSIONS[%d] different "
2239 : "from the one of shape",
2240 : i);
2241 0 : return false;
2242 : }
2243 : }
2244 :
2245 : // Try to load the indexing variable.
2246 : // Not in m_oMapMDArrays,
2247 : // then stat() the indexing variable.
2248 1054 : else if (osArrayName != osDimName &&
2249 1054 : m_oMapMDArrays.find(osDimName) == m_oMapMDArrays.end())
2250 : {
2251 972 : std::string osDirName = m_osDirectoryName;
2252 : while (true)
2253 : {
2254 : const std::string osArrayFilenameDim = CPLFormFilenameSafe(
2255 2184 : CPLFormFilenameSafe(osDirName.c_str(), osDimName.c_str(),
2256 : nullptr)
2257 : .c_str(),
2258 2184 : "zarr.json", nullptr);
2259 : VSIStatBufL sStat;
2260 2184 : if (VSIStatL(osArrayFilenameDim.c_str(), &sStat) == 0)
2261 : {
2262 4 : CPLJSONDocument oDoc;
2263 2 : if (oDoc.Load(osArrayFilenameDim))
2264 : {
2265 2 : LoadArray(osDimName, osArrayFilenameDim,
2266 4 : oDoc.GetRoot());
2267 : }
2268 : }
2269 : else
2270 : {
2271 : // Recurse to upper level for datasets such as
2272 : // /vsis3/hrrrzarr/sfc/20210809/20210809_00z_anl.zarr/0.1_sigma_level/HAIL_max_fcst/0.1_sigma_level/HAIL_max_fcst
2273 : std::string osDirNameNew =
2274 2182 : CPLGetPathSafe(osDirName.c_str());
2275 2182 : if (!osDirNameNew.empty() && osDirNameNew != osDirName)
2276 : {
2277 1698 : osDirName = std::move(osDirNameNew);
2278 1698 : continue;
2279 : }
2280 : }
2281 486 : break;
2282 1698 : }
2283 : }
2284 :
2285 568 : oIter = m_oMapDimensions.find(osDimName);
2286 : // cppcheck-suppress knownConditionTrueFalse
2287 570 : if (oIter != m_oMapDimensions.end() &&
2288 2 : oIter->second->GetSize() == poDim->GetSize())
2289 : {
2290 2 : poDim = oIter->second;
2291 2 : return true;
2292 : }
2293 :
2294 1132 : std::string osType;
2295 1132 : std::string osDirection;
2296 566 : if (aoDims.size() == 1 && osArrayName == osDimName)
2297 : {
2298 82 : ZarrArray::GetDimensionTypeDirection(oAttributes, osType,
2299 : osDirection);
2300 : }
2301 :
2302 : auto poDimLocal = std::make_shared<ZarrDimension>(
2303 566 : m_poSharedResource,
2304 1132 : std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
2305 1132 : GetFullName(), osDimName, osType, osDirection, poDim->GetSize());
2306 566 : poDimLocal->SetXArrayDimension();
2307 566 : m_oMapDimensions[osDimName] = poDimLocal;
2308 566 : poDim = poDimLocal;
2309 566 : return true;
2310 1170 : };
2311 :
2312 1170 : if (dimensionNames.GetType() == CPLJSONObject::Type::Array)
2313 : {
2314 407 : const auto arrayDims = dimensionNames.ToArray();
2315 407 : if (arrayDims.Size() == oShape.Size())
2316 : {
2317 1128 : for (int i = 0; i < oShape.Size(); ++i)
2318 : {
2319 722 : if (arrayDims[i].GetType() == CPLJSONObject::Type::String)
2320 : {
2321 2166 : const auto osDimName = arrayDims[i].ToString();
2322 722 : FindDimension(osDimName, aoDims[i], i);
2323 : }
2324 : }
2325 : }
2326 : else
2327 : {
2328 1 : CPLError(
2329 : CE_Failure, CPLE_AppDefined,
2330 : "Size of dimension_names[] different from the one of shape");
2331 1 : return nullptr;
2332 : }
2333 : }
2334 763 : else if (dimensionNames.IsValid())
2335 : {
2336 1 : CPLError(CE_Failure, CPLE_AppDefined,
2337 : "dimension_names should be an array");
2338 1 : return nullptr;
2339 : }
2340 :
2341 3504 : auto oDtype = oRoot["data_type"];
2342 1168 : if (!oDtype.IsValid())
2343 : {
2344 1 : CPLError(CE_Failure, CPLE_NotSupported, "data_type missing");
2345 1 : return nullptr;
2346 : }
2347 2334 : const auto oOrigDtype = oDtype;
2348 1167 : if (oDtype["fallback"].IsValid())
2349 1 : oDtype = oDtype["fallback"];
2350 2334 : std::vector<DtypeElt> aoDtypeElts;
2351 2334 : const auto oType = ParseDtypeV3(oDtype, aoDtypeElts);
2352 2323 : if (oType.GetClass() == GEDTC_NUMERIC &&
2353 1156 : oType.GetNumericDataType() == GDT_Unknown)
2354 2 : return nullptr;
2355 :
2356 2330 : std::vector<GUInt64> anOuterBlockSize;
2357 1165 : if (!ZarrArray::ParseChunkSize(oChunks, oType, anOuterBlockSize))
2358 1 : return nullptr;
2359 :
2360 2328 : std::vector<GByte> abyNoData;
2361 :
2362 : struct NoDataFreer
2363 : {
2364 : std::vector<GByte> &m_abyNodata;
2365 : const GDALExtendedDataType &m_oType;
2366 :
2367 1164 : NoDataFreer(std::vector<GByte> &abyNoDataIn,
2368 : const GDALExtendedDataType &oTypeIn)
2369 1164 : : m_abyNodata(abyNoDataIn), m_oType(oTypeIn)
2370 : {
2371 1164 : }
2372 :
2373 1164 : ~NoDataFreer()
2374 1164 : {
2375 1164 : if (!m_abyNodata.empty())
2376 1019 : m_oType.FreeDynamicMemory(&m_abyNodata[0]);
2377 1164 : }
2378 : };
2379 :
2380 2328 : NoDataFreer noDataFreer(abyNoData, oType);
2381 :
2382 3492 : auto oFillValue = oRoot["fill_value"];
2383 1164 : auto eFillValueType = oFillValue.GetType();
2384 :
2385 1164 : if (!oFillValue.IsValid())
2386 : {
2387 0 : CPLError(CE_Warning, CPLE_AppDefined, "Missing fill_value is invalid");
2388 : }
2389 1164 : else if (eFillValueType == CPLJSONObject::Type::Null)
2390 : {
2391 129 : CPLError(CE_Warning, CPLE_AppDefined, "fill_value = null is invalid");
2392 : }
2393 1035 : else if (GDALDataTypeIsComplex(oType.GetNumericDataType()) &&
2394 : eFillValueType != CPLJSONObject::Type::Array)
2395 : {
2396 4 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2397 4 : return nullptr;
2398 : }
2399 1031 : else if (eFillValueType == CPLJSONObject::Type::String)
2400 : {
2401 594 : const auto osFillValue = oFillValue.ToString();
2402 297 : if (oType.GetClass() == GEDTC_STRING)
2403 : {
2404 6 : abyNoData.resize(oType.GetSize());
2405 6 : char *pDstStr = CPLStrdup(osFillValue.c_str());
2406 6 : char **pDstPtr = reinterpret_cast<char **>(&abyNoData[0]);
2407 6 : memcpy(pDstPtr, &pDstStr, sizeof(pDstStr));
2408 : }
2409 291 : else if (STARTS_WITH(osFillValue.c_str(), "0x"))
2410 : {
2411 3 : if (osFillValue.size() > 2 + 2 * oType.GetSize())
2412 : {
2413 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2414 1 : return nullptr;
2415 : }
2416 : uint64_t nVal = static_cast<uint64_t>(
2417 3 : std::strtoull(osFillValue.c_str() + 2, nullptr, 16));
2418 3 : if (oType.GetSize() == 4)
2419 : {
2420 1 : abyNoData.resize(oType.GetSize());
2421 1 : uint32_t nTmp = static_cast<uint32_t>(nVal);
2422 1 : memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
2423 : }
2424 2 : else if (oType.GetSize() == 8)
2425 : {
2426 1 : abyNoData.resize(oType.GetSize());
2427 1 : memcpy(&abyNoData[0], &nVal, sizeof(nVal));
2428 : }
2429 : else
2430 : {
2431 1 : CPLError(CE_Failure, CPLE_AppDefined,
2432 : "Hexadecimal representation of fill_value no "
2433 : "supported for this data type");
2434 1 : return nullptr;
2435 : }
2436 : }
2437 288 : else if (STARTS_WITH(osFillValue.c_str(), "0b"))
2438 : {
2439 3 : if (osFillValue.size() > 2 + 8 * oType.GetSize())
2440 : {
2441 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2442 1 : return nullptr;
2443 : }
2444 : uint64_t nVal = static_cast<uint64_t>(
2445 3 : std::strtoull(osFillValue.c_str() + 2, nullptr, 2));
2446 3 : if (oType.GetSize() == 4)
2447 : {
2448 1 : abyNoData.resize(oType.GetSize());
2449 1 : uint32_t nTmp = static_cast<uint32_t>(nVal);
2450 1 : memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
2451 : }
2452 2 : else if (oType.GetSize() == 8)
2453 : {
2454 1 : abyNoData.resize(oType.GetSize());
2455 1 : memcpy(&abyNoData[0], &nVal, sizeof(nVal));
2456 : }
2457 : else
2458 : {
2459 1 : CPLError(CE_Failure, CPLE_AppDefined,
2460 : "Binary representation of fill_value no supported for "
2461 : "this data type");
2462 1 : return nullptr;
2463 : }
2464 : }
2465 : else
2466 : {
2467 : // Handle "NaT" fill_value for numpy.datetime64/timedelta64
2468 : // NaT is equivalent to INT64_MIN per the zarr extension spec
2469 285 : if (osFillValue == "NaT" && oType.GetNumericDataType() == GDT_Int64)
2470 : {
2471 4 : const int64_t nNaT = std::numeric_limits<int64_t>::min();
2472 4 : abyNoData.resize(oType.GetSize());
2473 4 : memcpy(&abyNoData[0], &nNaT, sizeof(nNaT));
2474 : }
2475 : else
2476 : {
2477 281 : bool bOK = true;
2478 : double dfNoDataValue =
2479 281 : ParseNoDataStringAsDouble(osFillValue, bOK);
2480 281 : if (!bOK)
2481 : {
2482 1 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2483 2 : return nullptr;
2484 : }
2485 280 : else if (oType.GetNumericDataType() == GDT_Float16)
2486 : {
2487 : const GFloat16 hfNoDataValue =
2488 1 : static_cast<GFloat16>(dfNoDataValue);
2489 1 : abyNoData.resize(sizeof(hfNoDataValue));
2490 1 : memcpy(&abyNoData[0], &hfNoDataValue,
2491 : sizeof(hfNoDataValue));
2492 : }
2493 279 : else if (oType.GetNumericDataType() == GDT_Float32)
2494 : {
2495 254 : const float fNoDataValue =
2496 254 : static_cast<float>(dfNoDataValue);
2497 254 : abyNoData.resize(sizeof(fNoDataValue));
2498 254 : memcpy(&abyNoData[0], &fNoDataValue, sizeof(fNoDataValue));
2499 : }
2500 25 : else if (oType.GetNumericDataType() == GDT_Float64)
2501 : {
2502 24 : abyNoData.resize(sizeof(dfNoDataValue));
2503 24 : memcpy(&abyNoData[0], &dfNoDataValue,
2504 : sizeof(dfNoDataValue));
2505 : }
2506 : else
2507 : {
2508 1 : CPLError(CE_Failure, CPLE_AppDefined,
2509 : "Invalid fill_value for this data type");
2510 1 : return nullptr;
2511 : }
2512 : }
2513 : }
2514 : }
2515 734 : else if (eFillValueType == CPLJSONObject::Type::Boolean ||
2516 535 : eFillValueType == CPLJSONObject::Type::Integer ||
2517 535 : eFillValueType == CPLJSONObject::Type::Long ||
2518 : eFillValueType == CPLJSONObject::Type::Double)
2519 : {
2520 710 : const double dfNoDataValue = oFillValue.ToDouble();
2521 710 : if (oType.GetNumericDataType() == GDT_Int64)
2522 : {
2523 : const int64_t nNoDataValue =
2524 140 : static_cast<int64_t>(oFillValue.ToLong());
2525 140 : abyNoData.resize(oType.GetSize());
2526 140 : GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
2527 : oType.GetNumericDataType(), 0, 1);
2528 : }
2529 570 : else if (oType.GetNumericDataType() == GDT_UInt64 &&
2530 : /* we can't really deal with nodata value between */
2531 : /* int64::max and uint64::max due to json-c limitations */
2532 0 : dfNoDataValue >= 0)
2533 : {
2534 : const int64_t nNoDataValue =
2535 0 : static_cast<int64_t>(oFillValue.ToLong());
2536 0 : abyNoData.resize(oType.GetSize());
2537 0 : GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
2538 : oType.GetNumericDataType(), 0, 1);
2539 : }
2540 : else
2541 : {
2542 570 : abyNoData.resize(oType.GetSize());
2543 570 : GDALCopyWords(&dfNoDataValue, GDT_Float64, 0, &abyNoData[0],
2544 : oType.GetNumericDataType(), 0, 1);
2545 710 : }
2546 : }
2547 24 : else if (eFillValueType == CPLJSONObject::Type::Array)
2548 : {
2549 24 : const auto oFillValueArray = oFillValue.ToArray();
2550 44 : if (oFillValueArray.Size() == 2 &&
2551 20 : GDALDataTypeIsComplex(oType.GetNumericDataType()))
2552 : {
2553 20 : if (oType.GetNumericDataType() == GDT_CFloat64)
2554 : {
2555 10 : bool bOK = true;
2556 : const double adfNoDataValue[2] = {
2557 10 : ParseNoDataComponent<double, uint64_t>(oFillValueArray[0],
2558 : bOK),
2559 10 : ParseNoDataComponent<double, uint64_t>(oFillValueArray[1],
2560 : bOK),
2561 20 : };
2562 10 : if (!bOK)
2563 : {
2564 2 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2565 2 : return nullptr;
2566 : }
2567 8 : abyNoData.resize(oType.GetSize());
2568 8 : CPLAssert(sizeof(adfNoDataValue) == oType.GetSize());
2569 8 : memcpy(abyNoData.data(), adfNoDataValue,
2570 : sizeof(adfNoDataValue));
2571 : }
2572 : else
2573 : {
2574 10 : CPLAssert(oType.GetNumericDataType() == GDT_CFloat32);
2575 10 : bool bOK = true;
2576 : const float afNoDataValue[2] = {
2577 10 : ParseNoDataComponent<float, uint32_t>(oFillValueArray[0],
2578 : bOK),
2579 10 : ParseNoDataComponent<float, uint32_t>(oFillValueArray[1],
2580 : bOK),
2581 20 : };
2582 10 : if (!bOK)
2583 : {
2584 2 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2585 2 : return nullptr;
2586 : }
2587 8 : abyNoData.resize(oType.GetSize());
2588 8 : CPLAssert(sizeof(afNoDataValue) == oType.GetSize());
2589 8 : memcpy(abyNoData.data(), afNoDataValue, sizeof(afNoDataValue));
2590 : }
2591 : }
2592 : else
2593 : {
2594 4 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2595 4 : return nullptr;
2596 : }
2597 : }
2598 : else
2599 : {
2600 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
2601 0 : return nullptr;
2602 : }
2603 :
2604 3444 : const auto oCodecs = oRoot["codecs"].ToArray();
2605 1148 : std::unique_ptr<ZarrV3CodecSequence> poCodecs;
2606 2296 : std::vector<GUInt64> anInnerBlockSize = anOuterBlockSize;
2607 1148 : if (oCodecs.Size() > 0)
2608 : {
2609 2236 : poCodecs = ZarrV3Array::SetupCodecs(oCodecs, anOuterBlockSize,
2610 : anInnerBlockSize,
2611 2236 : aoDtypeElts.back(), abyNoData);
2612 1118 : if (!poCodecs)
2613 : {
2614 18 : return nullptr;
2615 : }
2616 : }
2617 :
2618 1130 : auto poArray = ZarrV3Array::Create(m_poSharedResource, Self(), osArrayName,
2619 : aoDims, oType, aoDtypeElts,
2620 2260 : anOuterBlockSize, anInnerBlockSize);
2621 1130 : if (!poArray)
2622 1 : return nullptr;
2623 1129 : poArray->SetUpdatable(m_bUpdatable); // must be set before SetAttributes()
2624 1129 : poArray->SetFilename(osZarrayFilename);
2625 1129 : poArray->SetIsV2ChunkKeyEncoding(bV2ChunkKeyEncoding);
2626 1129 : poArray->SetDimSeparator(osDimSeparator);
2627 1129 : if (!abyNoData.empty())
2628 : {
2629 1000 : poArray->RegisterNoDataValue(abyNoData.data());
2630 : }
2631 1129 : poArray->SetAttributes(Self(), oAttributes);
2632 1129 : poArray->SetDtype(oDtype);
2633 : // Expose extension data type configuration as structural info
2634 1129 : if (oOrigDtype.GetType() == CPLJSONObject::Type::Object)
2635 : {
2636 24 : const auto osName = oOrigDtype.GetString("name");
2637 8 : if (!osName.empty())
2638 : {
2639 8 : poArray->SetStructuralInfo("data_type.name", osName.c_str());
2640 : }
2641 24 : const auto oConfig = oOrigDtype["configuration"];
2642 16 : if (oConfig.IsValid() &&
2643 8 : oConfig.GetType() == CPLJSONObject::Type::Object)
2644 : {
2645 24 : const auto osUnit = oConfig.GetString("unit");
2646 8 : if (!osUnit.empty())
2647 : {
2648 5 : poArray->SetStructuralInfo("data_type.unit", osUnit.c_str());
2649 : }
2650 8 : const auto nScaleFactor = oConfig.GetInteger("scale_factor", -1);
2651 8 : if (nScaleFactor > 0)
2652 : {
2653 4 : poArray->SetStructuralInfo("data_type.scale_factor",
2654 : CPLSPrintf("%d", nScaleFactor));
2655 : }
2656 : }
2657 : }
2658 2229 : if (oCodecs.Size() > 0 &&
2659 2229 : oCodecs[oCodecs.Size() - 1].GetString("name") != "bytes")
2660 : {
2661 1396 : poArray->SetStructuralInfo(
2662 1396 : "COMPRESSOR", oCodecs[oCodecs.Size() - 1].ToString().c_str());
2663 : }
2664 1129 : if (poCodecs)
2665 1100 : poArray->SetCodecs(oCodecs, std::move(poCodecs));
2666 1129 : RegisterArray(poArray);
2667 :
2668 : // If this is an indexing variable, attach it to the dimension.
2669 1129 : if (aoDims.size() == 1 && aoDims[0]->GetName() == poArray->GetName())
2670 : {
2671 82 : auto oIter = m_oMapDimensions.find(poArray->GetName());
2672 82 : if (oIter != m_oMapDimensions.end())
2673 : {
2674 82 : oIter->second->SetIndexingVariable(poArray);
2675 : }
2676 : }
2677 :
2678 1129 : if (CPLTestBool(m_poSharedResource->GetOpenOptions().FetchNameValueDef(
2679 : "CACHE_TILE_PRESENCE", "NO")))
2680 : {
2681 5 : poArray->BlockCachePresence();
2682 : }
2683 :
2684 1129 : return poArray;
2685 : }
2686 :
2687 : /************************************************************************/
2688 : /* ZarrV3Array::GetRawBlockInfoInfo() */
2689 : /************************************************************************/
2690 :
2691 6 : CPLStringList ZarrV3Array::GetRawBlockInfoInfo() const
2692 : {
2693 6 : CPLStringList aosInfo(m_aosStructuralInfo);
2694 6 : if (m_oType.GetSize() > 1)
2695 : {
2696 : // By default, assume that the ENDIANNESS is the native one.
2697 : // Otherwise there will be a ZarrV3CodecBytes instance.
2698 : if constexpr (CPL_IS_LSB)
2699 5 : aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
2700 : else
2701 : aosInfo.SetNameValue("ENDIANNESS", "BIG");
2702 : }
2703 :
2704 6 : if (m_poCodecs)
2705 : {
2706 6 : bool bHasOtherCodec = false;
2707 11 : for (const auto &poCodec : m_poCodecs->GetCodecs())
2708 : {
2709 6 : if (poCodec->GetName() == ZarrV3CodecBytes::NAME &&
2710 1 : m_oType.GetSize() > 1)
2711 : {
2712 : auto poBytesCodec =
2713 1 : dynamic_cast<const ZarrV3CodecBytes *>(poCodec.get());
2714 1 : if (poBytesCodec)
2715 : {
2716 1 : if (poBytesCodec->IsLittle())
2717 0 : aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
2718 : else
2719 1 : aosInfo.SetNameValue("ENDIANNESS", "BIG");
2720 : }
2721 : }
2722 5 : else if (poCodec->GetName() == ZarrV3CodecTranspose::NAME &&
2723 1 : m_aoDims.size() > 1)
2724 : {
2725 : auto poTransposeCodec =
2726 1 : dynamic_cast<const ZarrV3CodecTranspose *>(poCodec.get());
2727 1 : if (poTransposeCodec && !poTransposeCodec->IsNoOp())
2728 : {
2729 1 : const auto &anOrder = poTransposeCodec->GetOrder();
2730 1 : const int nDims = static_cast<int>(anOrder.size());
2731 2 : std::string osOrder("[");
2732 3 : for (int i = 0; i < nDims; ++i)
2733 : {
2734 2 : if (i > 0)
2735 1 : osOrder += ',';
2736 2 : osOrder += std::to_string(anOrder[i]);
2737 : }
2738 1 : osOrder += ']';
2739 1 : aosInfo.SetNameValue("TRANSPOSE_ORDER", osOrder.c_str());
2740 : }
2741 : }
2742 5 : else if (poCodec->GetName() != ZarrV3CodecGZip::NAME &&
2743 5 : poCodec->GetName() != ZarrV3CodecBlosc::NAME &&
2744 2 : poCodec->GetName() != ZarrV3CodecZstd::NAME)
2745 : {
2746 2 : bHasOtherCodec = true;
2747 : }
2748 : }
2749 6 : if (bHasOtherCodec)
2750 : {
2751 2 : aosInfo.SetNameValue("CODECS", m_oJSONCodecs.ToString().c_str());
2752 : }
2753 :
2754 6 : if (m_poCodecs->SupportsPartialDecoding())
2755 : {
2756 2 : aosInfo.SetNameValue("CHUNK_TYPE", "INNER");
2757 : }
2758 : }
2759 :
2760 6 : return aosInfo;
2761 : }
2762 :
2763 : /************************************************************************/
2764 : /* ZarrV3Array::SetupCodecs() */
2765 : /************************************************************************/
2766 :
2767 1312 : /* static */ std::unique_ptr<ZarrV3CodecSequence> ZarrV3Array::SetupCodecs(
2768 : const CPLJSONArray &oCodecs, const std::vector<GUInt64> &anOuterBlockSize,
2769 : std::vector<GUInt64> &anInnerBlockSize, DtypeElt &zarrDataType,
2770 : const std::vector<GByte> &abyNoData)
2771 :
2772 : {
2773 : // Byte swapping will be done by the codec chain
2774 1312 : zarrDataType.needByteSwapping = false;
2775 :
2776 2624 : ZarrArrayMetadata oInputArrayMetadata;
2777 1312 : if (!abyNoData.empty() && zarrDataType.gdalTypeIsApproxOfNative)
2778 : {
2779 : // This cannot happen today with the data types we support, but
2780 : // might in the future. In which case we'll have to translate the
2781 : // nodata value from its GDAL representation to the native one
2782 : // (since that's what zarr_v3_codec_sharding::FillWithNoData()
2783 : // expects
2784 0 : CPLError(CE_Warning, CPLE_AppDefined,
2785 : "Zarr driver issue: gdalTypeIsApproxOfNative is not taken "
2786 : "into account by codecs. Nodata will be assumed to be zero by "
2787 : "sharding codec");
2788 : }
2789 2301 : else if (!abyNoData.empty() &&
2790 989 : (zarrDataType.nativeType == DtypeElt::NativeType::STRING_ASCII ||
2791 984 : zarrDataType.nativeType == DtypeElt::NativeType::STRING_UNICODE))
2792 : {
2793 : // Convert from GDAL representation (char* pointer) to native
2794 : // format (fixed-size null-padded buffer) for FillWithNoData()
2795 6 : char *pStr = nullptr;
2796 6 : memcpy(&pStr, abyNoData.data(), sizeof(pStr));
2797 6 : oInputArrayMetadata.abyNoData.resize(zarrDataType.nativeSize, 0);
2798 12 : if (pStr &&
2799 6 : zarrDataType.nativeType == DtypeElt::NativeType::STRING_ASCII)
2800 : {
2801 : const size_t nCopy =
2802 15 : std::min(strlen(pStr), zarrDataType.nativeSize > 0
2803 5 : ? zarrDataType.nativeSize - 1
2804 5 : : static_cast<size_t>(0));
2805 5 : memcpy(oInputArrayMetadata.abyNoData.data(), pStr, nCopy);
2806 : }
2807 : // STRING_UNICODE non-empty fill would need UTF-8 to UCS4
2808 : // conversion; zero-fill is correct for the common "" case
2809 : }
2810 : else
2811 : {
2812 1306 : oInputArrayMetadata.abyNoData = abyNoData;
2813 : }
2814 3730 : for (auto &nSize : anOuterBlockSize)
2815 : {
2816 2418 : oInputArrayMetadata.anBlockSizes.push_back(static_cast<size_t>(nSize));
2817 : }
2818 1312 : oInputArrayMetadata.oElt = zarrDataType;
2819 2624 : auto poCodecs = std::make_unique<ZarrV3CodecSequence>(oInputArrayMetadata);
2820 2624 : ZarrArrayMetadata oOutputArrayMetadata;
2821 1312 : if (!poCodecs->InitFromJson(oCodecs, oOutputArrayMetadata))
2822 : {
2823 18 : return nullptr;
2824 : }
2825 2588 : std::vector<size_t> anOuterBlockSizeSizet;
2826 3676 : for (auto nVal : oOutputArrayMetadata.anBlockSizes)
2827 : {
2828 2382 : anOuterBlockSizeSizet.push_back(static_cast<size_t>(nVal));
2829 : }
2830 1294 : anInnerBlockSize.clear();
2831 3676 : for (size_t nVal : poCodecs->GetInnerMostBlockSize(anOuterBlockSizeSizet))
2832 : {
2833 2382 : anInnerBlockSize.push_back(nVal);
2834 : }
2835 1294 : return poCodecs;
2836 : }
2837 :
2838 : /************************************************************************/
2839 : /* ZarrV3Array::SetCodecs() */
2840 : /************************************************************************/
2841 :
2842 1294 : void ZarrV3Array::SetCodecs(const CPLJSONArray &oJSONCodecs,
2843 : std::unique_ptr<ZarrV3CodecSequence> &&poCodecs)
2844 : {
2845 1294 : m_oJSONCodecs = oJSONCodecs;
2846 1294 : m_poCodecs = std::move(poCodecs);
2847 1294 : }
2848 :
2849 : /************************************************************************/
2850 : /* ZarrV3Array::LoadOverviews() */
2851 : /************************************************************************/
2852 :
2853 99 : void ZarrV3Array::LoadOverviews() const
2854 : {
2855 99 : if (m_bOverviewsLoaded)
2856 51 : return;
2857 56 : m_bOverviewsLoaded = true;
2858 :
2859 : // Cf https://github.com/zarr-conventions/multiscales
2860 : // and https://github.com/zarr-conventions/spatial
2861 :
2862 56 : const auto poRG = GetRootGroup();
2863 56 : if (!poRG)
2864 : {
2865 1 : CPLError(CE_Warning, CPLE_AppDefined,
2866 : "LoadOverviews(): cannot access root group");
2867 1 : return;
2868 : }
2869 :
2870 55 : auto poGroup = GetParentGroup();
2871 55 : if (!poGroup)
2872 : {
2873 0 : CPLDebugOnly(ZARR_DEBUG_KEY,
2874 : "LoadOverviews(): cannot access parent group");
2875 0 : return;
2876 : }
2877 :
2878 : // Look for "zarr_conventions" and "multiscales" attributes in our
2879 : // immediate parent, or in our grandparent if not found.
2880 110 : auto poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
2881 110 : auto poAttrMultiscales = poGroup->GetAttribute("multiscales");
2882 55 : if (!poAttrZarrConventions || !poAttrMultiscales)
2883 : {
2884 43 : poGroup = poGroup->GetParentGroup();
2885 43 : if (poGroup)
2886 : {
2887 43 : poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
2888 43 : poAttrMultiscales = poGroup->GetAttribute("multiscales");
2889 : }
2890 43 : if (!poAttrZarrConventions || !poAttrMultiscales)
2891 : {
2892 1 : return;
2893 : }
2894 : }
2895 :
2896 54 : const char *pszZarrConventions = poAttrZarrConventions->ReadAsString();
2897 54 : const char *pszMultiscales = poAttrMultiscales->ReadAsString();
2898 54 : if (!pszZarrConventions || !pszMultiscales)
2899 0 : return;
2900 :
2901 54 : CPLJSONDocument oDoc;
2902 54 : if (!oDoc.LoadMemory(pszZarrConventions))
2903 0 : return;
2904 54 : const auto oZarrConventions = oDoc.GetRoot();
2905 :
2906 54 : if (!oDoc.LoadMemory(pszMultiscales))
2907 0 : return;
2908 54 : const auto oMultiscales = oDoc.GetRoot();
2909 :
2910 54 : if (!oZarrConventions.IsValid() ||
2911 54 : oZarrConventions.GetType() != CPLJSONObject::Type::Array ||
2912 162 : !oMultiscales.IsValid() ||
2913 54 : oMultiscales.GetType() != CPLJSONObject::Type::Object)
2914 : {
2915 0 : return;
2916 : }
2917 :
2918 54 : const auto oZarrConventionsArray = oZarrConventions.ToArray();
2919 54 : const auto hasMultiscalesUUIDLambda = [](const CPLJSONObject &obj)
2920 54 : { return obj.GetString("uuid") == ZARR_MULTISCALES_UUID; };
2921 : const bool bFoundMultiScalesUUID =
2922 108 : std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
2923 162 : hasMultiscalesUUIDLambda) != oZarrConventionsArray.end();
2924 54 : if (!bFoundMultiScalesUUID)
2925 0 : return;
2926 :
2927 68 : const auto hasSpatialUUIDLambda = [](const CPLJSONObject &obj)
2928 : {
2929 68 : constexpr const char *SPATIAL_UUID =
2930 : "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4";
2931 68 : return obj.GetString("uuid") == SPATIAL_UUID;
2932 : };
2933 : const bool bFoundSpatialUUID =
2934 108 : std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
2935 162 : hasSpatialUUIDLambda) != oZarrConventionsArray.end();
2936 :
2937 108 : const auto oLayout = oMultiscales["layout"];
2938 54 : if (!oLayout.IsValid() && oLayout.GetType() != CPLJSONObject::Type::Array)
2939 : {
2940 1 : CPLError(CE_Warning, CPLE_AppDefined,
2941 : "layout not found in multiscales");
2942 1 : return;
2943 : }
2944 :
2945 : // is pixel-is-area ?
2946 106 : auto poSpatialRegistration = poGroup->GetAttribute("spatial:registration");
2947 : const char *pszSpatialRegistration =
2948 53 : poSpatialRegistration ? poSpatialRegistration->ReadAsString() : nullptr;
2949 53 : const bool bHasExplicitPixelSpatialRegistration =
2950 67 : bFoundSpatialUUID && pszSpatialRegistration &&
2951 14 : strcmp(pszSpatialRegistration, "pixel") == 0;
2952 :
2953 53 : std::vector<std::string> aosSpatialDimensions;
2954 53 : std::set<std::string> oSetSpatialDimensionNames;
2955 106 : auto poSpatialDimensions = poGroup->GetAttribute("spatial:dimensions");
2956 53 : if (bFoundSpatialUUID && poSpatialDimensions)
2957 : {
2958 14 : aosSpatialDimensions = poSpatialDimensions->ReadAsStringArray();
2959 42 : for (const auto &osDimName : aosSpatialDimensions)
2960 : {
2961 28 : oSetSpatialDimensionNames.insert(osDimName);
2962 : }
2963 : }
2964 :
2965 : // Multiscales convention: asset/derived_from paths are relative to
2966 : // the group holding the convention metadata, not the store root.
2967 53 : const std::string osGroupPrefix = poGroup->GetFullName().back() == '/'
2968 51 : ? poGroup->GetFullName()
2969 104 : : poGroup->GetFullName() + '/';
2970 : const auto resolveAssetPath =
2971 240 : [&osGroupPrefix](const std::string &osRelative) -> std::string
2972 240 : { return osGroupPrefix + osRelative; };
2973 :
2974 : // Check whether this multiscales describes our array's pyramid.
2975 : // The first layout entry is the base (full-resolution) level; its
2976 : // "asset" field identifies the target array. If it refers to a
2977 : // different array, the entire layout is irrelevant to us.
2978 : {
2979 53 : const auto oFirstItem = oLayout.ToArray()[0];
2980 106 : const std::string osBaseAsset = oFirstItem.GetString("asset");
2981 53 : if (!osBaseAsset.empty())
2982 : {
2983 0 : std::shared_ptr<GDALGroup> poBaseGroup;
2984 : {
2985 52 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
2986 : poBaseGroup =
2987 52 : poRG->OpenGroupFromFullname(resolveAssetPath(osBaseAsset));
2988 : }
2989 52 : if (poBaseGroup)
2990 : {
2991 : // Group-based layout (e.g. OME-Zarr "0", "1", ...):
2992 : // skip if the base group has no array with our name.
2993 33 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
2994 33 : if (!poBaseGroup->OpenMDArray(GetName()))
2995 0 : return;
2996 : }
2997 : else
2998 : {
2999 : // Extract array name from path (e.g. "level0/ar" -> "ar").
3000 19 : const auto nSlash = osBaseAsset.rfind('/');
3001 : const std::string osArrayName =
3002 : nSlash != std::string::npos ? osBaseAsset.substr(nSlash + 1)
3003 19 : : osBaseAsset;
3004 19 : if (osArrayName != GetName())
3005 5 : return;
3006 : }
3007 : }
3008 : }
3009 :
3010 183 : for (const auto &oLayoutItem : oLayout.ToArray())
3011 : {
3012 270 : const std::string osAsset = oLayoutItem.GetString("asset");
3013 135 : if (osAsset.empty())
3014 : {
3015 1 : CPLError(CE_Warning, CPLE_AppDefined,
3016 : "multiscales.layout[].asset not found");
3017 1 : continue;
3018 : }
3019 :
3020 : // Resolve "asset" to a MDArray
3021 0 : std::shared_ptr<GDALGroup> poAssetGroup;
3022 : {
3023 134 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
3024 : poAssetGroup =
3025 134 : poRG->OpenGroupFromFullname(resolveAssetPath(osAsset));
3026 : }
3027 0 : std::shared_ptr<GDALMDArray> poAssetArray;
3028 134 : if (poAssetGroup)
3029 : {
3030 110 : poAssetArray = poAssetGroup->OpenMDArray(GetName());
3031 : }
3032 24 : else if (osAsset.find('/') == std::string::npos)
3033 : {
3034 9 : poAssetArray = poGroup->OpenMDArray(osAsset);
3035 9 : if (!poAssetArray)
3036 : {
3037 0 : CPLError(CE_Warning, CPLE_AppDefined,
3038 : "multiscales.layout[].asset=%s ignored, because it is "
3039 : "not a valid group or array name",
3040 : osAsset.c_str());
3041 0 : continue;
3042 : }
3043 : }
3044 : else
3045 : {
3046 : poAssetArray =
3047 15 : poRG->OpenMDArrayFromFullname(resolveAssetPath(osAsset));
3048 15 : if (poAssetArray && poAssetArray->GetName() != GetName())
3049 : {
3050 0 : continue;
3051 : }
3052 : }
3053 134 : if (!poAssetArray)
3054 : {
3055 10 : continue;
3056 : }
3057 124 : if (poAssetArray->GetDimensionCount() != GetDimensionCount())
3058 : {
3059 3 : CPLError(
3060 : CE_Warning, CPLE_AppDefined,
3061 : "multiscales.layout[].asset=%s (%s) ignored, because it has "
3062 : "not the same dimension count as %s (%s)",
3063 1 : osAsset.c_str(), poAssetArray->GetFullName().c_str(),
3064 2 : GetName().c_str(), GetFullName().c_str());
3065 1 : continue;
3066 : }
3067 123 : if (poAssetArray->GetDataType() != GetDataType())
3068 : {
3069 3 : CPLError(
3070 : CE_Warning, CPLE_AppDefined,
3071 : "multiscales.layout[].asset=%s (%s) ignored, because it has "
3072 : "not the same data type as %s (%s)",
3073 1 : osAsset.c_str(), poAssetArray->GetFullName().c_str(),
3074 2 : GetName().c_str(), GetFullName().c_str());
3075 1 : continue;
3076 : }
3077 :
3078 122 : bool bAssetIsDownsampledOfThis = false;
3079 239 : for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
3080 : {
3081 358 : if (poAssetArray->GetDimensions()[iDim]->GetSize() <
3082 179 : GetDimensions()[iDim]->GetSize())
3083 : {
3084 62 : bAssetIsDownsampledOfThis = true;
3085 62 : break;
3086 : }
3087 : }
3088 122 : if (!bAssetIsDownsampledOfThis)
3089 : {
3090 : // not an error
3091 60 : continue;
3092 : }
3093 :
3094 : // Inspect dimensions of the asset
3095 62 : std::map<std::string, size_t> oMapAssetDimNameToIdx;
3096 62 : const auto &apoAssetDims = poAssetArray->GetDimensions();
3097 62 : size_t nCountSpatialDimsFoundInAsset = 0;
3098 184 : for (const auto &[idx, poDim] : cpl::enumerate(apoAssetDims))
3099 : {
3100 122 : oMapAssetDimNameToIdx[poDim->GetName()] = idx;
3101 122 : if (cpl::contains(oSetSpatialDimensionNames, poDim->GetName()))
3102 43 : ++nCountSpatialDimsFoundInAsset;
3103 : }
3104 : const bool bAssetHasAllSpatialDims =
3105 62 : (nCountSpatialDimsFoundInAsset == aosSpatialDimensions.size());
3106 :
3107 : // Consistency checks on "derived_from" and "transform"
3108 124 : const auto oDerivedFrom = oLayoutItem["derived_from"];
3109 124 : const auto oTransform = oLayoutItem["transform"];
3110 97 : if (oDerivedFrom.IsValid() && oTransform.IsValid() &&
3111 132 : oDerivedFrom.GetType() == CPLJSONObject::Type::String &&
3112 35 : oTransform.GetType() == CPLJSONObject::Type::Object)
3113 : {
3114 70 : const std::string osDerivedFrom = oDerivedFrom.ToString();
3115 : // Resolve "derived_from" to a MDArray
3116 0 : std::shared_ptr<GDALGroup> poDerivedFromGroup;
3117 : {
3118 35 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
3119 105 : poDerivedFromGroup = poRG->OpenGroupFromFullname(
3120 105 : resolveAssetPath(osDerivedFrom));
3121 : }
3122 0 : std::shared_ptr<GDALMDArray> poDerivedFromArray;
3123 35 : if (poDerivedFromGroup)
3124 : {
3125 21 : poDerivedFromArray = poDerivedFromGroup->OpenMDArray(GetName());
3126 : }
3127 14 : else if (osDerivedFrom.find('/') == std::string::npos)
3128 : {
3129 10 : poDerivedFromArray = poGroup->OpenMDArray(osDerivedFrom);
3130 10 : if (!poDerivedFromArray)
3131 : {
3132 1 : CPLError(CE_Warning, CPLE_AppDefined,
3133 : "multiscales.layout[].asset=%s refers to "
3134 : "derived_from=%s which does not exist",
3135 : osAsset.c_str(), osDerivedFrom.c_str());
3136 1 : poDerivedFromArray.reset();
3137 : }
3138 : }
3139 : else
3140 : {
3141 12 : poDerivedFromArray = poRG->OpenMDArrayFromFullname(
3142 12 : resolveAssetPath(osDerivedFrom));
3143 : }
3144 35 : if (poDerivedFromArray && bAssetHasAllSpatialDims)
3145 : {
3146 33 : if (poDerivedFromArray->GetDimensionCount() !=
3147 33 : GetDimensionCount())
3148 : {
3149 1 : CPLError(CE_Warning, CPLE_AppDefined,
3150 : "multiscales.layout[].asset=%s refers to "
3151 : "derived_from=%s that does not have the expected "
3152 : "number of dimensions. Ignoring that asset",
3153 : osAsset.c_str(), osDerivedFrom.c_str());
3154 3 : continue;
3155 : }
3156 :
3157 64 : const auto oScale = oTransform["scale"];
3158 32 : if (oScale.GetType() == CPLJSONObject::Type::Array &&
3159 : bHasExplicitPixelSpatialRegistration)
3160 : {
3161 10 : const auto oScaleArray = oScale.ToArray();
3162 10 : if (oScaleArray.size() != GetDimensionCount())
3163 : {
3164 :
3165 1 : CPLError(CE_Warning, CPLE_AppDefined,
3166 : "multiscales.layout[].asset=%s has a "
3167 : "transform.scale array with an unexpected "
3168 : "number of values. Ignoring the asset",
3169 : osAsset.c_str());
3170 1 : continue;
3171 : }
3172 :
3173 27 : for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
3174 : {
3175 18 : const double dfScale = oScaleArray[iDim].ToDouble();
3176 : const double dfExpectedScale =
3177 18 : static_cast<double>(
3178 18 : poDerivedFromArray->GetDimensions()[iDim]
3179 18 : ->GetSize()) /
3180 18 : static_cast<double>(
3181 18 : poAssetArray->GetDimensions()[iDim]->GetSize());
3182 18 : constexpr double EPSILON = 1e-3;
3183 18 : if (std::fabs(dfScale - dfExpectedScale) >
3184 18 : EPSILON * dfExpectedScale)
3185 : {
3186 2 : CPLError(CE_Warning, CPLE_AppDefined,
3187 : "multiscales.layout[].asset=%s has a "
3188 : "transform.scale[%d]=%f value whereas %f "
3189 : "was expected. "
3190 : "Assuming that later value as the scale.",
3191 : osAsset.c_str(), static_cast<int>(iDim),
3192 : dfScale, dfExpectedScale);
3193 : }
3194 : }
3195 : }
3196 :
3197 62 : const auto oTranslation = oTransform["translation"];
3198 31 : if (oTranslation.GetType() == CPLJSONObject::Type::Array &&
3199 : bHasExplicitPixelSpatialRegistration)
3200 : {
3201 9 : const auto oTranslationArray = oTranslation.ToArray();
3202 9 : if (oTranslationArray.size() != GetDimensionCount())
3203 : {
3204 1 : CPLError(CE_Warning, CPLE_AppDefined,
3205 : "multiscales.layout[].asset=%s has a "
3206 : "transform.translation array with an "
3207 : "unexpected number of values. "
3208 : "Ignoring the asset",
3209 : osAsset.c_str());
3210 1 : continue;
3211 : }
3212 :
3213 24 : for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
3214 : {
3215 : const double dfOffset =
3216 16 : oTranslationArray[iDim].ToDouble();
3217 16 : if (dfOffset != 0)
3218 : {
3219 1 : CPLError(CE_Warning, CPLE_AppDefined,
3220 : "multiscales.layout[].asset=%s has a "
3221 : "transform.translation[%d]=%f value. "
3222 : "Ignoring that offset.",
3223 : osAsset.c_str(), static_cast<int>(iDim),
3224 : dfOffset);
3225 : }
3226 : }
3227 : }
3228 : }
3229 : }
3230 :
3231 59 : if (bFoundSpatialUUID && bAssetHasAllSpatialDims)
3232 : {
3233 38 : const auto oSpatialShape = oLayoutItem["spatial:shape"];
3234 19 : if (oSpatialShape.IsValid())
3235 : {
3236 19 : if (oSpatialShape.GetType() != CPLJSONObject::Type::Array)
3237 : {
3238 1 : CPLError(
3239 : CE_Warning, CPLE_AppDefined,
3240 : "multiscales.layout[].asset=%s ignored, because its "
3241 : "spatial:shape property is not an array",
3242 : osAsset.c_str());
3243 3 : continue;
3244 : }
3245 18 : const auto oSpatialShapeArray = oSpatialShape.ToArray();
3246 18 : if (oSpatialShapeArray.size() != aosSpatialDimensions.size())
3247 : {
3248 1 : CPLError(
3249 : CE_Warning, CPLE_AppDefined,
3250 : "multiscales.layout[].asset=%s ignored, because its "
3251 : "spatial:shape property has not the expected number "
3252 : "of values",
3253 : osAsset.c_str());
3254 1 : continue;
3255 : }
3256 :
3257 17 : bool bSkip = false;
3258 68 : for (const auto &[idx, oShapeVal] :
3259 85 : cpl::enumerate(oSpatialShapeArray))
3260 : {
3261 : const auto oIter =
3262 34 : oMapAssetDimNameToIdx.find(aosSpatialDimensions[idx]);
3263 34 : if (oIter != oMapAssetDimNameToIdx.end())
3264 : {
3265 68 : const auto poDim = apoAssetDims[oIter->second];
3266 34 : if (poDim->GetSize() !=
3267 34 : static_cast<uint64_t>(oShapeVal.ToLong()))
3268 : {
3269 1 : bSkip = true;
3270 1 : CPLError(CE_Warning, CPLE_AppDefined,
3271 : "multiscales.layout[].asset=%s ignored, "
3272 : "because its "
3273 : "spatial:shape[%d] value is %" PRIu64
3274 : " whereas %" PRIu64 " was expected.",
3275 1 : osAsset.c_str(), static_cast<int>(idx),
3276 1 : static_cast<uint64_t>(oShapeVal.ToLong()),
3277 1 : static_cast<uint64_t>(poDim->GetSize()));
3278 : }
3279 : }
3280 : }
3281 17 : if (bSkip)
3282 1 : continue;
3283 : }
3284 : }
3285 :
3286 56 : m_apoOverviews.push_back(std::move(poAssetArray));
3287 : }
3288 : }
3289 :
3290 : /************************************************************************/
3291 : /* ZarrV3Array::GetOverviewCount() */
3292 : /************************************************************************/
3293 :
3294 99 : int ZarrV3Array::GetOverviewCount() const
3295 : {
3296 99 : LoadOverviews();
3297 99 : return static_cast<int>(m_apoOverviews.size());
3298 : }
3299 :
3300 : /************************************************************************/
3301 : /* ZarrV3Array::GetOverview() */
3302 : /************************************************************************/
3303 :
3304 54 : std::shared_ptr<GDALMDArray> ZarrV3Array::GetOverview(int idx) const
3305 : {
3306 54 : if (idx < 0 || idx >= GetOverviewCount())
3307 12 : return nullptr;
3308 42 : return m_apoOverviews[idx];
3309 : }
3310 :
3311 : /************************************************************************/
3312 : /* ZarrV3Array::ReconstructCreationOptionsFromCodecs() */
3313 : /************************************************************************/
3314 :
3315 : // When an array is opened from disk (LoadArray), m_aosCreationOptions is
3316 : // empty because SetCreationOptions() is only called during CreateMDArray().
3317 : // BuildOverviews() needs the creation options so that overview arrays
3318 : // inherit the same codec (compression, sharding). This method reverse-maps
3319 : // the stored codec JSON back to creation option key/value pairs.
3320 :
3321 9 : void ZarrV3Array::ReconstructCreationOptionsFromCodecs()
3322 : {
3323 9 : if (!m_poCodecs || m_aosCreationOptions.FetchNameValue("COMPRESS"))
3324 1 : return;
3325 :
3326 16 : CPLJSONArray oCodecArray = m_poCodecs->GetJSon().ToArray();
3327 :
3328 : // Detect sharding: if the sole top-level codec is sharding_indexed,
3329 : // extract SHARD_CHUNK_SHAPE and use the inner codecs for compression.
3330 15 : for (int i = 0; i < oCodecArray.Size(); ++i)
3331 : {
3332 8 : const auto oCodec = oCodecArray[i];
3333 8 : if (oCodec.GetString("name") == "sharding_indexed")
3334 : {
3335 3 : const auto oConfig = oCodec["configuration"];
3336 :
3337 : // Inner chunk shape
3338 3 : const auto oChunkShape = oConfig.GetArray("chunk_shape");
3339 1 : if (oChunkShape.IsValid() && oChunkShape.Size() > 0)
3340 : {
3341 2 : std::string osShape;
3342 3 : for (int j = 0; j < oChunkShape.Size(); ++j)
3343 : {
3344 2 : if (!osShape.empty())
3345 1 : osShape += ',';
3346 : osShape += CPLSPrintf(
3347 : CPL_FRMT_GUIB,
3348 2 : static_cast<GUIntBig>(oChunkShape[j].ToLong()));
3349 : }
3350 : m_aosCreationOptions.SetNameValue("SHARD_CHUNK_SHAPE",
3351 1 : osShape.c_str());
3352 : }
3353 :
3354 : // Use inner codecs for compression detection
3355 1 : oCodecArray = oConfig.GetArray("codecs");
3356 1 : break;
3357 : }
3358 : }
3359 :
3360 : // Scan codecs for compression algorithm
3361 17 : for (int i = 0; i < oCodecArray.Size(); ++i)
3362 : {
3363 18 : const auto oCodec = oCodecArray[i];
3364 27 : const auto osName = oCodec.GetString("name");
3365 27 : const auto oConfig = oCodec["configuration"];
3366 :
3367 9 : if (osName == "gzip")
3368 : {
3369 0 : m_aosCreationOptions.SetNameValue("COMPRESS", "GZIP");
3370 0 : if (oConfig.IsValid())
3371 : {
3372 : m_aosCreationOptions.SetNameValue(
3373 : "GZIP_LEVEL",
3374 0 : CPLSPrintf("%d", oConfig.GetInteger("level")));
3375 : }
3376 : }
3377 9 : else if (osName == "zstd")
3378 : {
3379 1 : m_aosCreationOptions.SetNameValue("COMPRESS", "ZSTD");
3380 1 : if (oConfig.IsValid())
3381 : {
3382 : m_aosCreationOptions.SetNameValue(
3383 : "ZSTD_LEVEL",
3384 1 : CPLSPrintf("%d", oConfig.GetInteger("level")));
3385 1 : if (oConfig.GetBool("checksum"))
3386 0 : m_aosCreationOptions.SetNameValue("ZSTD_CHECKSUM", "YES");
3387 : }
3388 : }
3389 8 : else if (osName == "blosc")
3390 : {
3391 0 : m_aosCreationOptions.SetNameValue("COMPRESS", "BLOSC");
3392 0 : if (oConfig.IsValid())
3393 : {
3394 0 : const auto osCName = oConfig.GetString("cname");
3395 0 : if (!osCName.empty())
3396 : m_aosCreationOptions.SetNameValue("BLOSC_CNAME",
3397 0 : osCName.c_str());
3398 : m_aosCreationOptions.SetNameValue(
3399 : "BLOSC_CLEVEL",
3400 0 : CPLSPrintf("%d", oConfig.GetInteger("clevel")));
3401 0 : const auto osShuffle = oConfig.GetString("shuffle");
3402 0 : if (osShuffle == "noshuffle")
3403 0 : m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "NONE");
3404 0 : else if (osShuffle == "bitshuffle")
3405 0 : m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "BIT");
3406 : else
3407 0 : m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "BYTE");
3408 : }
3409 : }
3410 : }
3411 : }
3412 :
3413 : /************************************************************************/
3414 : /* ZarrV3Array::BuildOverviews() */
3415 : /************************************************************************/
3416 :
3417 14 : CPLErr ZarrV3Array::BuildOverviews(const char *pszResampling, int nOverviews,
3418 : const int *panOverviewList,
3419 : GDALProgressFunc pfnProgress,
3420 : void *pProgressData,
3421 : CSLConstList /* papszOptions */)
3422 : {
3423 14 : const size_t nDimCount = GetDimensionCount();
3424 14 : if (nDimCount < 2)
3425 : {
3426 1 : CPLError(CE_Failure, CPLE_AppDefined,
3427 : "BuildOverviews() requires at least 2 dimensions");
3428 1 : return CE_Failure;
3429 : }
3430 :
3431 13 : if (!m_bUpdatable)
3432 : {
3433 1 : CPLError(CE_Failure, CPLE_AppDefined,
3434 : "Dataset not open in update mode");
3435 1 : return CE_Failure;
3436 : }
3437 :
3438 : auto poParentGroup =
3439 24 : std::static_pointer_cast<ZarrV3Group>(GetParentGroup());
3440 12 : if (!poParentGroup)
3441 : {
3442 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot access parent group");
3443 0 : return CE_Failure;
3444 : }
3445 :
3446 12 : if (!pfnProgress)
3447 11 : pfnProgress = GDALDummyProgress;
3448 :
3449 : // Identify spatial dimensions via GDALDimension::GetType().
3450 : // Fall back to last two dimensions (Y, X) if types are not set.
3451 12 : const auto &apoSrcDims = GetDimensions();
3452 12 : size_t iYDim = nDimCount - 2;
3453 12 : size_t iXDim = nDimCount - 1;
3454 :
3455 37 : for (size_t i = 0; i < nDimCount; ++i)
3456 : {
3457 25 : if (apoSrcDims[i]->GetType() == GDAL_DIM_TYPE_HORIZONTAL_X)
3458 2 : iXDim = i;
3459 23 : else if (apoSrcDims[i]->GetType() == GDAL_DIM_TYPE_HORIZONTAL_Y)
3460 2 : iYDim = i;
3461 : }
3462 :
3463 12 : if (iXDim == iYDim)
3464 : {
3465 0 : CPLError(CE_Failure, CPLE_AppDefined,
3466 : "Cannot identify two distinct spatial dimensions. "
3467 : "Set dimension types (HORIZONTAL_X / HORIZONTAL_Y) "
3468 : "or ensure the array has at least 2 dimensions.");
3469 0 : return CE_Failure;
3470 : }
3471 :
3472 : // Delete existing overview groups (ovr_*) for idempotent rebuild.
3473 : // Also handles nOverviews==0 ("clear overviews").
3474 14 : for (const auto &osName : poParentGroup->GetGroupNames())
3475 : {
3476 4 : if (STARTS_WITH(osName.c_str(), "ovr_") &&
3477 2 : !poParentGroup->DeleteGroup(osName))
3478 : {
3479 0 : CPLError(CE_Failure, CPLE_AppDefined,
3480 : "Cannot delete existing overview group '%s'",
3481 : osName.c_str());
3482 0 : return CE_Failure;
3483 : }
3484 : }
3485 :
3486 12 : if (nOverviews == 0)
3487 : {
3488 1 : poParentGroup->GenerateMultiscalesMetadata(nullptr);
3489 1 : m_bOverviewsLoaded = false;
3490 1 : m_apoOverviews.clear();
3491 1 : return CE_None;
3492 : }
3493 :
3494 11 : if (nOverviews < 0 || !panOverviewList)
3495 : {
3496 0 : CPLError(CE_Failure, CPLE_IllegalArg, "Invalid overview list");
3497 0 : return CE_Failure;
3498 : }
3499 :
3500 11 : if (!pszResampling || pszResampling[0] == '\0')
3501 0 : pszResampling = "NEAREST";
3502 :
3503 : // Sort and deduplicate factors for sequential resampling chain.
3504 22 : std::vector<int> anFactors(panOverviewList, panOverviewList + nOverviews);
3505 11 : std::sort(anFactors.begin(), anFactors.end());
3506 11 : anFactors.erase(std::unique(anFactors.begin(), anFactors.end()),
3507 22 : anFactors.end());
3508 22 : for (const int nFactor : anFactors)
3509 : {
3510 13 : if (nFactor < 2)
3511 : {
3512 2 : CPLError(CE_Failure, CPLE_IllegalArg,
3513 : "Overview factor %d is invalid (must be >= 2)", nFactor);
3514 2 : return CE_Failure;
3515 : }
3516 : }
3517 :
3518 : // Ensure creation options are populated (they are empty when the array
3519 : // was opened from disk rather than freshly created).
3520 9 : ReconstructCreationOptionsFromCodecs();
3521 :
3522 : // Inherit creation options from source array (codec settings, etc.).
3523 : // Only override BLOCKSIZE and SHARD_CHUNK_SHAPE per level.
3524 18 : CPLStringList aosCreateOptions(m_aosCreationOptions);
3525 :
3526 9 : const std::string &osArrayName = GetName();
3527 9 : const void *pRawNoData = GetRawNoDataValue();
3528 :
3529 : // Build each level sequentially: 2x from base, 4x from 2x, etc.
3530 : // poChainSource starts as the base array and advances to each
3531 : // newly created overview so each level resamples from the previous.
3532 : std::shared_ptr<GDALMDArray> poChainSource =
3533 18 : std::dynamic_pointer_cast<GDALMDArray>(m_pSelf.lock());
3534 9 : if (!poChainSource)
3535 : {
3536 0 : CPLError(CE_Failure, CPLE_AppDefined,
3537 : "Cannot obtain shared_ptr to self");
3538 0 : return CE_Failure;
3539 : }
3540 :
3541 : // Pre-compute total output pixels for pixel-weighted progress.
3542 9 : double dfTotalPixels = 0.0;
3543 20 : for (const int nF : anFactors)
3544 : {
3545 33 : dfTotalPixels += static_cast<double>(
3546 22 : DIV_ROUND_UP(apoSrcDims[iYDim]->GetSize(), nF)) *
3547 11 : DIV_ROUND_UP(apoSrcDims[iXDim]->GetSize(), nF);
3548 : }
3549 9 : double dfPixelsProcessed = 0.0;
3550 :
3551 9 : const int nFactorCount = static_cast<int>(anFactors.size());
3552 20 : for (int iOvr = 0; iOvr < nFactorCount; ++iOvr)
3553 : {
3554 11 : const int nFactor = anFactors[iOvr];
3555 :
3556 : // Create sibling group for this overview level.
3557 11 : const std::string osGroupName = CPLSPrintf("ovr_%dx", nFactor);
3558 11 : auto poOvrGroup = poParentGroup->CreateGroup(osGroupName);
3559 11 : if (!poOvrGroup)
3560 : {
3561 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot create group '%s'",
3562 : osGroupName.c_str());
3563 0 : return CE_Failure;
3564 : }
3565 :
3566 : // Create dimensions: downsample spatial, preserve non-spatial.
3567 11 : std::vector<std::shared_ptr<GDALDimension>> aoOvrDims;
3568 11 : std::string osBlockSize;
3569 34 : for (size_t i = 0; i < nDimCount; ++i)
3570 : {
3571 23 : const bool bSpatial = (i == iYDim || i == iXDim);
3572 23 : const GUInt64 nSrcSize = apoSrcDims[i]->GetSize();
3573 23 : const GUInt64 nOvrSize =
3574 23 : bSpatial ? DIV_ROUND_UP(nSrcSize, nFactor) : nSrcSize;
3575 :
3576 23 : auto poDim = poOvrGroup->CreateDimension(
3577 46 : apoSrcDims[i]->GetName(), apoSrcDims[i]->GetType(),
3578 69 : apoSrcDims[i]->GetDirection(), nOvrSize);
3579 23 : if (!poDim)
3580 : {
3581 0 : CPLError(CE_Failure, CPLE_AppDefined,
3582 : "Cannot create dimension '%s' in group '%s'",
3583 0 : apoSrcDims[i]->GetName().c_str(), osGroupName.c_str());
3584 0 : return CE_Failure;
3585 : }
3586 23 : aoOvrDims.push_back(std::move(poDim));
3587 :
3588 : // Block size: inherit from source, cap to overview dim size.
3589 23 : const GUInt64 nBlock = std::min(m_anOuterBlockSize[i], nOvrSize);
3590 23 : if (!osBlockSize.empty())
3591 12 : osBlockSize += ',';
3592 : osBlockSize +=
3593 23 : CPLSPrintf(CPL_FRMT_GUIB, static_cast<GUIntBig>(nBlock));
3594 :
3595 : // Build 1D coordinate array for spatial dimensions.
3596 23 : if (bSpatial)
3597 : {
3598 22 : auto poSrcVar = apoSrcDims[i]->GetIndexingVariable();
3599 22 : if (poSrcVar && poSrcVar->GetDimensionCount() == 1)
3600 : {
3601 4 : if (nOvrSize > 100 * 1000 * 1000)
3602 : {
3603 0 : CPLError(CE_Failure, CPLE_AppDefined,
3604 : "Overview dimension too large for "
3605 : "coordinate array");
3606 0 : return CE_Failure;
3607 : }
3608 4 : const size_t nOvrCount = static_cast<size_t>(nOvrSize);
3609 4 : auto poCoordArray = poOvrGroup->CreateMDArray(
3610 4 : poSrcVar->GetName(), {aoOvrDims.back()},
3611 16 : poSrcVar->GetDataType());
3612 4 : if (poCoordArray)
3613 : {
3614 4 : std::vector<double> adfValues;
3615 : try
3616 : {
3617 4 : adfValues.resize(nOvrCount);
3618 : }
3619 0 : catch (const std::exception &)
3620 : {
3621 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
3622 : "Cannot allocate coordinate array");
3623 0 : return CE_Failure;
3624 : }
3625 4 : double dfStart = 0;
3626 4 : double dfIncrement = 0;
3627 4 : if (poSrcVar->IsRegularlySpaced(dfStart, dfIncrement))
3628 : {
3629 : // Recalculate from spacing: overview pixels are
3630 : // centered at (j * factor + (factor-1)/2.0) in
3631 : // source pixel space.
3632 87 : for (size_t j = 0; j < nOvrCount; ++j)
3633 : {
3634 84 : adfValues[j] =
3635 84 : dfStart +
3636 84 : (static_cast<double>(j) * nFactor +
3637 84 : (nFactor - 1) / 2.0) *
3638 : dfIncrement;
3639 : }
3640 : }
3641 : else
3642 : {
3643 : // Irregular spacing: subsample by stride.
3644 1 : if (nSrcSize > 100 * 1000 * 1000)
3645 : {
3646 0 : CPLError(CE_Failure, CPLE_AppDefined,
3647 : "Source dimension too large "
3648 : "for coordinate array");
3649 0 : return CE_Failure;
3650 : }
3651 1 : const size_t nSrcCount =
3652 : static_cast<size_t>(nSrcSize);
3653 1 : std::vector<double> adfSrc;
3654 : try
3655 : {
3656 1 : adfSrc.resize(nSrcCount);
3657 : }
3658 0 : catch (const std::exception &)
3659 : {
3660 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
3661 : "Cannot allocate source "
3662 : "coordinate array");
3663 0 : return CE_Failure;
3664 : }
3665 1 : const GUInt64 anSrcStart[1] = {0};
3666 1 : const size_t anSrcCount[1] = {nSrcCount};
3667 2 : if (!poSrcVar->Read(
3668 : anSrcStart, anSrcCount, nullptr, nullptr,
3669 2 : GDALExtendedDataType::Create(GDT_Float64),
3670 1 : adfSrc.data()))
3671 : {
3672 0 : CPLError(CE_Failure, CPLE_AppDefined,
3673 : "Failed to read coordinate "
3674 : "variable '%s'",
3675 0 : poSrcVar->GetName().c_str());
3676 0 : return CE_Failure;
3677 : }
3678 33 : for (size_t j = 0; j < nOvrCount; ++j)
3679 : {
3680 : // Pick the source index closest to the
3681 : // overview pixel center.
3682 : const size_t nSrcIdx = std::min(
3683 64 : static_cast<size_t>(static_cast<double>(j) *
3684 32 : nFactor +
3685 32 : nFactor / 2),
3686 32 : nSrcCount - 1);
3687 32 : adfValues[j] = adfSrc[nSrcIdx];
3688 : }
3689 : }
3690 4 : const GUInt64 anStart[1] = {0};
3691 4 : const size_t anCount[1] = {nOvrCount};
3692 8 : if (!poCoordArray->Write(
3693 : anStart, anCount, nullptr, nullptr,
3694 8 : GDALExtendedDataType::Create(GDT_Float64),
3695 4 : adfValues.data()))
3696 : {
3697 0 : CPLError(CE_Failure, CPLE_AppDefined,
3698 : "Failed to write coordinate "
3699 : "variable for overview");
3700 0 : return CE_Failure;
3701 : }
3702 8 : aoOvrDims.back()->SetIndexingVariable(
3703 4 : std::move(poCoordArray));
3704 : }
3705 : }
3706 : }
3707 : }
3708 11 : aosCreateOptions.SetNameValue("BLOCKSIZE", osBlockSize.c_str());
3709 :
3710 : // Validate SHARD_CHUNK_SHAPE: inner chunks must divide
3711 : // the (capped) block size evenly. Drop sharding if not.
3712 : const char *pszShardShape =
3713 11 : aosCreateOptions.FetchNameValue("SHARD_CHUNK_SHAPE");
3714 11 : if (pszShardShape)
3715 : {
3716 : const CPLStringList aosShard(
3717 2 : CSLTokenizeString2(pszShardShape, ",", 0));
3718 : const CPLStringList aosBlock(
3719 2 : CSLTokenizeString2(osBlockSize.c_str(), ",", 0));
3720 1 : bool bShardValid = (aosShard.size() == aosBlock.size());
3721 3 : for (int iDim = 0; bShardValid && iDim < aosShard.size(); ++iDim)
3722 : {
3723 2 : const auto nInner = static_cast<GUInt64>(atoll(aosShard[iDim]));
3724 2 : const auto nOuter = static_cast<GUInt64>(atoll(aosBlock[iDim]));
3725 2 : if (nInner == 0 || nOuter < nInner || nOuter % nInner != 0)
3726 0 : bShardValid = false;
3727 : }
3728 1 : if (!bShardValid)
3729 0 : aosCreateOptions.SetNameValue("SHARD_CHUNK_SHAPE", nullptr);
3730 : }
3731 :
3732 11 : auto poOvrArray = poOvrGroup->CreateMDArray(
3733 11 : osArrayName, aoOvrDims, GetDataType(), aosCreateOptions.List());
3734 11 : if (!poOvrArray)
3735 : {
3736 0 : CPLError(CE_Failure, CPLE_AppDefined,
3737 : "Cannot create overview array for factor %d", nFactor);
3738 0 : return CE_Failure;
3739 : }
3740 :
3741 11 : if (pRawNoData)
3742 3 : poOvrArray->SetRawNoDataValue(pRawNoData);
3743 :
3744 : // Wrap as classic datasets for GDALRegenerateOverviews.
3745 : // Non-spatial dims become bands automatically.
3746 : std::unique_ptr<GDALDataset> poPrevDS(
3747 11 : poChainSource->AsClassicDataset(iXDim, iYDim));
3748 : std::unique_ptr<GDALDataset> poOvrDS(
3749 11 : poOvrArray->AsClassicDataset(iXDim, iYDim));
3750 11 : if (!poPrevDS || !poOvrDS)
3751 : {
3752 0 : CPLError(CE_Failure, CPLE_AppDefined,
3753 : "Cannot create classic dataset wrapper for resampling");
3754 0 : return CE_Failure;
3755 : }
3756 :
3757 : // Resample all bands from previous level into this overview.
3758 11 : const int nBands = poPrevDS->GetRasterCount();
3759 : const double dfLevelPixels =
3760 11 : static_cast<double>(poOvrDS->GetRasterXSize()) *
3761 11 : poOvrDS->GetRasterYSize();
3762 22 : void *pLevelData = GDALCreateScaledProgress(
3763 : dfPixelsProcessed / dfTotalPixels,
3764 11 : (dfPixelsProcessed + dfLevelPixels) / dfTotalPixels, pfnProgress,
3765 : pProgressData);
3766 :
3767 11 : CPLErr eErr = CE_None;
3768 24 : for (int iBand = 1; iBand <= nBands && eErr == CE_None; ++iBand)
3769 : {
3770 13 : const double dfBandBase = static_cast<double>(iBand - 1) / nBands;
3771 13 : const double dfBandEnd = static_cast<double>(iBand) / nBands;
3772 13 : void *pBandData = GDALCreateScaledProgress(
3773 : dfBandBase, dfBandEnd, GDALScaledProgress, pLevelData);
3774 :
3775 : GDALRasterBandH hOvrBand =
3776 13 : GDALRasterBand::ToHandle(poOvrDS->GetRasterBand(iBand));
3777 13 : eErr = GDALRegenerateOverviews(
3778 : GDALRasterBand::ToHandle(poPrevDS->GetRasterBand(iBand)), 1,
3779 : &hOvrBand, pszResampling, GDALScaledProgress, pBandData);
3780 :
3781 13 : GDALDestroyScaledProgress(pBandData);
3782 : }
3783 :
3784 11 : GDALDestroyScaledProgress(pLevelData);
3785 11 : dfPixelsProcessed += dfLevelPixels;
3786 :
3787 11 : if (eErr != CE_None)
3788 : {
3789 0 : CPLError(CE_Failure, CPLE_AppDefined,
3790 : "GDALRegenerateOverviews failed for factor %d", nFactor);
3791 0 : return CE_Failure;
3792 : }
3793 :
3794 11 : poChainSource = std::move(poOvrArray);
3795 : }
3796 :
3797 : // Write multiscales metadata on parent group.
3798 9 : poParentGroup->GenerateMultiscalesMetadata(pszResampling);
3799 :
3800 : // Reset overview cache so GetOverviewCount() rediscovers.
3801 9 : m_bOverviewsLoaded = false;
3802 9 : m_apoOverviews.clear();
3803 :
3804 9 : return CE_None;
3805 : }
|