LCOV - code coverage report
Current view: top level - frmts/zarr - zarr_v3_array.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 1548 1795 86.2 %
Date: 2026-03-05 10:33:42 Functions: 49 51 96.1 %

          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             : }

Generated by: LCOV version 1.14