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