LCOV - code coverage report
Current view: top level - frmts/stacit - stacitdataset.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 423 502 84.3 %
Date: 2025-10-01 17:07:58 Functions: 14 14 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GDAL
       4             :  * Purpose:  STACIT (Spatio-Temporal Asset Catalog ITems) 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_json.h"
      14             : #include "cpl_http.h"
      15             : #include "gdal_frmts.h"
      16             : #include "gdal_driver.h"
      17             : #include "gdal_drivermanager.h"
      18             : #include "gdal_openinfo.h"
      19             : #include "gdal_cpp_functions.h"
      20             : #include "vrtdataset.h"
      21             : #include "ogr_spatialref.h"
      22             : 
      23             : #include <algorithm>
      24             : #include <limits>
      25             : #include <map>
      26             : #include <string>
      27             : 
      28             : namespace
      29             : {
      30             : 
      31             : struct AssetItem
      32             : {
      33             :     std::string osFilename{};
      34             :     std::string osDateTime{};
      35             :     int nXSize = 0;
      36             :     int nYSize = 0;
      37             :     double dfXMin = 0;
      38             :     double dfYMin = 0;
      39             :     double dfXMax = 0;
      40             :     double dfYMax = 0;
      41             : };
      42             : 
      43             : struct AssetSetByProjection
      44             : {
      45             :     std::string osProjUserString{};
      46             :     std::vector<AssetItem> assets{};
      47             : };
      48             : 
      49             : struct Asset
      50             : {
      51             :     std::string osName{};
      52             :     CPLJSONArray bands{};
      53             :     std::map<std::string, AssetSetByProjection> assets{};
      54             : };
      55             : 
      56             : struct Collection
      57             : {
      58             :     std::string osName{};
      59             :     std::map<std::string, Asset> assets{};
      60             : };
      61             : }  // namespace
      62             : 
      63             : /************************************************************************/
      64             : /*                            STACITDataset                             */
      65             : /************************************************************************/
      66             : 
      67          42 : class STACITDataset final : public VRTDataset
      68             : {
      69             :     bool Open(GDALOpenInfo *poOpenInfo);
      70             :     bool SetupDataset(GDALOpenInfo *poOpenInfo,
      71             :                       const std::string &osSTACITFilename,
      72             :                       std::map<std::string, Collection> &oMapCollection);
      73             :     void
      74             :     SetSubdatasets(const std::string &osFilename,
      75             :                    const std::map<std::string, Collection> &oMapCollection);
      76             : 
      77             :   public:
      78             :     STACITDataset();
      79             :     ~STACITDataset() override;
      80             : 
      81             :     static int Identify(GDALOpenInfo *poOpenInfo);
      82             :     static GDALDataset *OpenStatic(GDALOpenInfo *poOpenInfo);
      83             : };
      84             : 
      85             : /************************************************************************/
      86             : /*                          STACITDataset()                             */
      87             : /************************************************************************/
      88             : 
      89          21 : STACITDataset::STACITDataset() : VRTDataset(0, 0)
      90             : {
      91          21 :     poDriver = nullptr;  // cancel what the VRTDataset did
      92          21 :     SetWritable(false);
      93          21 : }
      94             : 
      95             : STACITDataset::~STACITDataset() = default;
      96             : 
      97             : /************************************************************************/
      98             : /*                             Identify()                               */
      99             : /************************************************************************/
     100             : 
     101       57583 : int STACITDataset::Identify(GDALOpenInfo *poOpenInfo)
     102             : {
     103       57583 :     if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
     104             :     {
     105          12 :         return true;
     106             :     }
     107             : 
     108       57571 :     const bool bIsSingleDriver = poOpenInfo->IsSingleAllowedDriver("STACIT");
     109       57571 :     if (bIsSingleDriver && (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
     110           4 :                             STARTS_WITH(poOpenInfo->pszFilename, "https://")))
     111             :     {
     112           1 :         return true;
     113             :     }
     114             : 
     115       57570 :     if (poOpenInfo->nHeaderBytes == 0)
     116             :     {
     117       54417 :         return false;
     118             :     }
     119             : 
     120        3153 :     if (poOpenInfo->IsExtensionEqualToCI("zarr"))
     121           0 :         return false;
     122             : 
     123        9395 :     for (int i = 0; i < 2; i++)
     124             :     {
     125             :         // TryToIngest() may reallocate pabyHeader, so do not move this
     126             :         // before the loop.
     127        6274 :         const char *pszHeader =
     128             :             reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
     129        6670 :         while (*pszHeader != 0 &&
     130        4812 :                std::isspace(static_cast<unsigned char>(*pszHeader)))
     131         396 :             ++pszHeader;
     132        6274 :         if (bIsSingleDriver)
     133             :         {
     134           4 :             return pszHeader[0] == '{';
     135             :         }
     136             : 
     137        6270 :         if (strstr(pszHeader, "\"stac_version\"") != nullptr)
     138             :         {
     139          28 :             int nTransformBBOXShapeCount = 0;
     140          84 :             for (const char *pszItem :
     141         112 :                  {"\"proj:transform\"", "\"proj:bbox\"", "\"proj:shape\""})
     142             :             {
     143          84 :                 if (strstr(pszHeader, pszItem))
     144          56 :                     nTransformBBOXShapeCount++;
     145             :             }
     146          28 :             if (nTransformBBOXShapeCount >= 2)
     147             :             {
     148          28 :                 return true;
     149             :             }
     150             :         }
     151             : 
     152        6242 :         if (i == 0)
     153             :         {
     154             :             // Should be enough for a STACIT .json file
     155        3121 :             poOpenInfo->TryToIngest(32768);
     156             :         }
     157             :     }
     158             : 
     159        3121 :     return false;
     160             : }
     161             : 
     162             : /************************************************************************/
     163             : /*                        SanitizeCRSValue()                            */
     164             : /************************************************************************/
     165             : 
     166          27 : static std::string SanitizeCRSValue(const std::string &v)
     167             : {
     168          27 :     std::string ret;
     169          27 :     bool lastWasAlphaNum = true;
     170          87 :     for (char ch : v)
     171             :     {
     172          60 :         if (!isalnum(static_cast<unsigned char>(ch)))
     173             :         {
     174           6 :             if (lastWasAlphaNum)
     175           6 :                 ret += '_';
     176           6 :             lastWasAlphaNum = false;
     177             :         }
     178             :         else
     179             :         {
     180          54 :             ret += ch;
     181          54 :             lastWasAlphaNum = true;
     182             :         }
     183             :     }
     184          27 :     if (!ret.empty() && ret.back() == '_')
     185           0 :         ret.pop_back();
     186          27 :     return ret;
     187             : }
     188             : 
     189             : /************************************************************************/
     190             : /*                            ParseAsset()                              */
     191             : /************************************************************************/
     192             : 
     193          45 : static void ParseAsset(const CPLJSONObject &jAsset,
     194             :                        const CPLJSONObject &oProperties,
     195             :                        const std::string &osCollection,
     196             :                        const std::string &osFilteredCRS,
     197             :                        std::map<std::string, Collection> &oMapCollection)
     198             : {
     199             :     // Skip assets that are obviously not images
     200          90 :     const auto osType = jAsset["type"].ToString();
     201          84 :     if (osType == "application/json" || osType == "application/xml" ||
     202          39 :         osType == "text/plain")
     203             :     {
     204           6 :         return;
     205             :     }
     206             : 
     207             :     // Skip assets whose role is obviously non georeferenced
     208          78 :     const auto oRoles = jAsset.GetArray("roles");
     209          39 :     if (oRoles.IsValid())
     210             :     {
     211          78 :         for (const auto &oRole : oRoles)
     212             :         {
     213          78 :             const auto osRole = oRole.ToString();
     214          78 :             if (osRole == "thumbnail" || osRole == "info" ||
     215          39 :                 osRole == "metadata")
     216             :             {
     217           0 :                 return;
     218             :             }
     219             :         }
     220             :     }
     221             : 
     222          39 :     const auto osAssetName = jAsset.GetName();
     223             : 
     224          78 :     std::string osHref = jAsset["href"].ToString();
     225          39 :     if (osHref.empty())
     226             :     {
     227           0 :         CPLError(CE_Warning, CPLE_AppDefined, "Missing href on asset %s",
     228             :                  osAssetName.c_str());
     229           0 :         return;
     230             :     }
     231             : 
     232             :     const auto GetAssetOrFeatureProperty =
     233         301 :         [&oProperties, &jAsset](const char *pszName)
     234             :     {
     235         564 :         auto obj = jAsset[pszName];
     236         188 :         if (obj.IsValid())
     237          75 :             return obj;
     238         113 :         return oProperties[pszName];
     239          39 :     };
     240             : 
     241          39 :     std::string osProjUserString;
     242          39 :     const auto oProjCode = GetAssetOrFeatureProperty("proj:code");
     243          39 :     if (oProjCode.IsValid() && oProjCode.GetType() != CPLJSONObject::Type::Null)
     244             :     {
     245           1 :         osProjUserString = oProjCode.ToString();
     246             :     }
     247             :     else
     248             :     {
     249          38 :         const auto oProjEPSG = GetAssetOrFeatureProperty("proj:epsg");
     250          76 :         if (oProjEPSG.IsValid() &&
     251          38 :             oProjEPSG.GetType() != CPLJSONObject::Type::Null)
     252             :         {
     253          38 :             osProjUserString = "EPSG:" + oProjEPSG.ToString();
     254             :         }
     255             :         else
     256             :         {
     257           0 :             const auto oProjWKT2 = GetAssetOrFeatureProperty("proj:wkt2");
     258           0 :             if (oProjWKT2.IsValid() &&
     259           0 :                 oProjWKT2.GetType() == CPLJSONObject::Type::String)
     260             :             {
     261           0 :                 osProjUserString = oProjWKT2.ToString();
     262             :             }
     263             :             else
     264             :             {
     265             :                 const auto oProjPROJJSON =
     266           0 :                     GetAssetOrFeatureProperty("proj:projjson");
     267           0 :                 if (oProjPROJJSON.IsValid() &&
     268           0 :                     oProjPROJJSON.GetType() == CPLJSONObject::Type::Object)
     269             :                 {
     270           0 :                     osProjUserString = oProjPROJJSON.ToString();
     271             :                 }
     272             :                 else
     273             :                 {
     274           0 :                     CPLDebug("STACIT",
     275             :                              "Skipping asset %s that lacks a valid CRS member",
     276             :                              osAssetName.c_str());
     277           0 :                     return;
     278             :                 }
     279             :             }
     280             :         }
     281             :     }
     282             : 
     283          43 :     if (!osFilteredCRS.empty() &&
     284          43 :         osFilteredCRS != SanitizeCRSValue(osProjUserString))
     285             :     {
     286           2 :         return;
     287             :     }
     288             : 
     289          37 :     AssetItem item;
     290          37 :     item.osFilename = std::move(osHref);
     291          37 :     item.osDateTime = oProperties["datetime"].ToString();
     292             : 
     293             :     // Figure out item bounds and width/height
     294          37 :     auto oProjBBOX = GetAssetOrFeatureProperty("proj:bbox").ToArray();
     295          37 :     auto oProjShape = GetAssetOrFeatureProperty("proj:shape").ToArray();
     296          37 :     auto oProjTransform = GetAssetOrFeatureProperty("proj:transform").ToArray();
     297          37 :     const bool bIsBBOXValid = oProjBBOX.IsValid() && oProjBBOX.Size() == 4;
     298          37 :     const bool bIsShapeValid = oProjShape.IsValid() && oProjShape.Size() == 2;
     299             :     const bool bIsTransformValid =
     300          74 :         oProjTransform.IsValid() &&
     301          37 :         (oProjTransform.Size() == 6 || oProjTransform.Size() == 9);
     302          37 :     std::vector<double> bbox;
     303          37 :     if (bIsBBOXValid)
     304             :     {
     305         130 :         for (const auto &oItem : oProjBBOX)
     306         104 :             bbox.push_back(oItem.ToDouble());
     307          26 :         CPLAssert(bbox.size() == 4);
     308             :     }
     309          37 :     std::vector<int> shape;
     310          37 :     if (bIsShapeValid)
     311             :     {
     312          33 :         for (const auto &oItem : oProjShape)
     313          22 :             shape.push_back(oItem.ToInteger());
     314          11 :         CPLAssert(shape.size() == 2);
     315             :     }
     316          37 :     std::vector<double> transform;
     317          37 :     if (bIsTransformValid)
     318             :     {
     319         352 :         for (const auto &oItem : oProjTransform)
     320         315 :             transform.push_back(oItem.ToDouble());
     321          37 :         CPLAssert(transform.size() == 6 || transform.size() == 9);
     322             : #if defined(__GNUC__)
     323             : #pragma GCC diagnostic push
     324             : #pragma GCC diagnostic ignored "-Wnull-dereference"
     325             : #endif
     326          74 :         if (transform[0] <= 0 || transform[1] != 0 || transform[3] != 0 ||
     327         142 :             transform[4] >= 0 ||
     328          37 :             (transform.size() == 9 &&
     329          31 :              (transform[6] != 0 || transform[7] != 0 || transform[8] != 1)))
     330             :         {
     331           0 :             CPLError(
     332             :                 CE_Warning, CPLE_AppDefined,
     333             :                 "Skipping asset %s because its proj:transform is "
     334             :                 "not of the form [xres,0,xoffset,0,yres<0,yoffset[,0,0,1]]",
     335             :                 osAssetName.c_str());
     336           0 :             return;
     337             :         }
     338             : #if defined(__GNUC__)
     339             : #pragma GCC diagnostic pop
     340             : #endif
     341             :     }
     342             : 
     343          37 :     if (bIsBBOXValid && bIsShapeValid)
     344             :     {
     345           0 :         item.nXSize = shape[1];
     346           0 :         item.nYSize = shape[0];
     347           0 :         item.dfXMin = bbox[0];
     348           0 :         item.dfYMin = bbox[1];
     349           0 :         item.dfXMax = bbox[2];
     350           0 :         item.dfYMax = bbox[3];
     351             :     }
     352          37 :     else if (bIsBBOXValid && bIsTransformValid)
     353             :     {
     354          26 :         item.dfXMin = bbox[0];
     355          26 :         item.dfYMin = bbox[1];
     356          26 :         item.dfXMax = bbox[2];
     357          26 :         item.dfYMax = bbox[3];
     358          26 :         if (item.dfXMin != transform[2] || item.dfYMax != transform[5])
     359             :         {
     360           0 :             CPLError(CE_Warning, CPLE_AppDefined,
     361             :                      "Skipping asset %s because the origin of "
     362             :                      "proj:transform and proj:bbox are not consistent",
     363             :                      osAssetName.c_str());
     364           0 :             return;
     365             :         }
     366          26 :         double dfXSize = (item.dfXMax - item.dfXMin) / transform[0];
     367          26 :         double dfYSize = (item.dfYMax - item.dfYMin) / -transform[4];
     368          26 :         if (!(dfXSize < INT_MAX && dfYSize < INT_MAX))
     369           0 :             return;
     370          26 :         item.nXSize = static_cast<int>(dfXSize);
     371          26 :         item.nYSize = static_cast<int>(dfYSize);
     372             :     }
     373          11 :     else if (bIsShapeValid && bIsTransformValid)
     374             :     {
     375          11 :         item.nXSize = shape[1];
     376          11 :         item.nYSize = shape[0];
     377          11 :         item.dfXMin = transform[2];
     378          11 :         item.dfYMax = transform[5];
     379          11 :         item.dfXMax = item.dfXMin + item.nXSize * transform[0];
     380          11 :         item.dfYMin = item.dfYMax + item.nYSize * transform[4];
     381             :     }
     382             :     else
     383             :     {
     384           0 :         CPLDebug("STACIT",
     385             :                  "Skipping asset %s that lacks at least 2 members among "
     386             :                  "proj:bbox, proj:shape and proj:transform",
     387             :                  osAssetName.c_str());
     388           0 :         return;
     389             :     }
     390             : 
     391          37 :     if (item.nXSize <= 0 || item.nYSize <= 0)
     392             :     {
     393           0 :         CPLError(CE_Warning, CPLE_AppDefined,
     394             :                  "Skipping asset %s because the size is invalid",
     395             :                  osAssetName.c_str());
     396           0 :         return;
     397             :     }
     398             : 
     399             :     // Create/fetch collection
     400          37 :     if (oMapCollection.find(osCollection) == oMapCollection.end())
     401             :     {
     402          40 :         Collection collection;
     403          20 :         collection.osName = osCollection;
     404          20 :         oMapCollection[osCollection] = std::move(collection);
     405             :     }
     406          37 :     auto &collection = oMapCollection[osCollection];
     407             : 
     408             :     // Create/fetch asset in collection
     409          37 :     if (collection.assets.find(osAssetName) == collection.assets.end())
     410             :     {
     411          42 :         Asset asset;
     412          21 :         asset.osName = osAssetName;
     413          21 :         asset.bands = jAsset.GetArray("bands");
     414          21 :         if (!asset.bands.IsValid())
     415          20 :             asset.bands = jAsset.GetArray("eo:bands");
     416             : 
     417          21 :         collection.assets[osAssetName] = std::move(asset);
     418             :     }
     419          37 :     auto &asset = collection.assets[osAssetName];
     420             : 
     421             :     // Create/fetch projection in asset
     422          37 :     if (asset.assets.find(osProjUserString) == asset.assets.end())
     423             :     {
     424          44 :         AssetSetByProjection assetByProj;
     425          22 :         assetByProj.osProjUserString = osProjUserString;
     426          22 :         asset.assets[osProjUserString] = std::move(assetByProj);
     427             :     }
     428          37 :     auto &assets = asset.assets[osProjUserString];
     429             : 
     430             :     // Add item
     431          37 :     assets.assets.emplace_back(std::move(item));
     432             : }
     433             : 
     434             : /************************************************************************/
     435             : /*                           SetupDataset()                             */
     436             : /************************************************************************/
     437             : 
     438          18 : bool STACITDataset::SetupDataset(
     439             :     GDALOpenInfo *poOpenInfo, const std::string &osSTACITFilename,
     440             :     std::map<std::string, Collection> &oMapCollection)
     441             : {
     442          18 :     auto &collection = oMapCollection.begin()->second;
     443          18 :     auto &asset = collection.assets.begin()->second;
     444          18 :     auto &assetByProj = asset.assets.begin()->second;
     445          18 :     auto &items = assetByProj.assets;
     446             : 
     447             :     // Compute global bounds and resolution
     448          18 :     double dfXMin = std::numeric_limits<double>::max();
     449          18 :     double dfYMin = std::numeric_limits<double>::max();
     450          18 :     double dfXMax = -std::numeric_limits<double>::max();
     451          18 :     double dfYMax = -std::numeric_limits<double>::max();
     452          18 :     double dfXRes = 0;
     453          18 :     double dfYRes = 0;
     454          36 :     const char *pszResolution = CSLFetchNameValueDef(
     455          18 :         poOpenInfo->papszOpenOptions, "RESOLUTION", "AVERAGE");
     456          51 :     for (const auto &assetItem : items)
     457             :     {
     458          33 :         dfXMin = std::min(dfXMin, assetItem.dfXMin);
     459          33 :         dfYMin = std::min(dfYMin, assetItem.dfYMin);
     460          33 :         dfXMax = std::max(dfXMax, assetItem.dfXMax);
     461          33 :         dfYMax = std::max(dfYMax, assetItem.dfYMax);
     462          33 :         const double dfThisXRes =
     463          33 :             (assetItem.dfXMax - assetItem.dfXMin) / assetItem.nXSize;
     464          33 :         const double dfThisYRes =
     465          33 :             (assetItem.dfYMax - assetItem.dfYMin) / assetItem.nYSize;
     466             : #ifdef DEBUG_VERBOSE
     467             :         CPLDebug("STACIT", "%s -> resx=%f resy=%f",
     468             :                  assetItem.osFilename.c_str(), dfThisXRes, dfThisYRes);
     469             : #endif
     470          33 :         if (dfXRes != 0 && EQUAL(pszResolution, "HIGHEST"))
     471             :         {
     472           0 :             dfXRes = std::min(dfXRes, dfThisXRes);
     473           0 :             dfYRes = std::min(dfYRes, dfThisYRes);
     474             :         }
     475          33 :         else if (dfXRes != 0 && EQUAL(pszResolution, "LOWEST"))
     476             :         {
     477           0 :             dfXRes = std::max(dfXRes, dfThisXRes);
     478           0 :             dfYRes = std::max(dfYRes, dfThisYRes);
     479             :         }
     480             :         else
     481             :         {
     482          33 :             dfXRes += dfThisXRes;
     483          33 :             dfYRes += dfThisYRes;
     484             :         }
     485             :     }
     486          18 :     if (EQUAL(pszResolution, "AVERAGE"))
     487             :     {
     488          18 :         dfXRes /= static_cast<int>(items.size());
     489          18 :         dfYRes /= static_cast<int>(items.size());
     490             :     }
     491             : 
     492             :     // Set raster size
     493          18 :     if (dfXRes == 0 || dfYRes == 0)
     494             :     {
     495           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     496             :                  "Invalid computed dataset dimensions");
     497           0 :         return false;
     498             :     }
     499          18 :     double dfXSize = std::round((dfXMax - dfXMin) / dfXRes);
     500          18 :     double dfYSize = std::round((dfYMax - dfYMin) / dfYRes);
     501          18 :     if (dfXSize <= 0 || dfYSize <= 0 || dfXSize > INT_MAX || dfYSize > INT_MAX)
     502             :     {
     503           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     504             :                  "Invalid computed dataset dimensions");
     505           0 :         return false;
     506             :     }
     507          18 :     nRasterXSize = static_cast<int>(dfXSize);
     508          18 :     nRasterYSize = static_cast<int>(dfYSize);
     509             : 
     510             :     // Set geotransform
     511          18 :     GDALGeoTransform gt{dfXMin, dfXRes, 0, dfYMax, 0, -dfYRes};
     512          18 :     SetGeoTransform(gt);
     513             : 
     514             :     // Set SRS
     515          36 :     OGRSpatialReference oSRS;
     516          18 :     if (oSRS.SetFromUserInput(
     517             :             assetByProj.osProjUserString.c_str(),
     518          18 :             OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
     519             :         OGRERR_NONE)
     520             :     {
     521          18 :         oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     522          18 :         SetSpatialRef(&oSRS);
     523             :     }
     524             : 
     525             :     // Open of the items to find the number of bands, their data type
     526             :     // and nodata.
     527             :     const auto BuildVSICurlFilename =
     528          51 :         [&osSTACITFilename, &collection](const std::string &osFilename)
     529             :     {
     530          51 :         std::string osRet;
     531          51 :         if (STARTS_WITH(osFilename.c_str(), "http"))
     532             :         {
     533           0 :             if (STARTS_WITH(osSTACITFilename.c_str(),
     534             :                             "https://planetarycomputer.microsoft.com/api/"))
     535             :             {
     536           0 :                 osRet = "/vsicurl?pc_url_signing=yes&pc_collection=";
     537           0 :                 osRet += collection.osName;
     538           0 :                 osRet += "&url=";
     539             :                 char *pszEncoded =
     540           0 :                     CPLEscapeString(osFilename.c_str(), -1, CPLES_URL);
     541           0 :                 CPLString osEncoded(pszEncoded);
     542           0 :                 CPLFree(pszEncoded);
     543             :                 // something is confused if the whole URL appears as
     544             :                 // /vsicurl...blabla_without_slash.tif" so transform back %2F to
     545             :                 // /
     546           0 :                 osEncoded.replaceAll("%2F", '/');
     547           0 :                 osRet += osEncoded;
     548             :             }
     549             :             else
     550             :             {
     551           0 :                 osRet = "/vsicurl/";
     552           0 :                 osRet += osFilename;
     553             :             }
     554             :         }
     555             :         else
     556             :         {
     557          51 :             osRet = VSIURIToVSIPath(osFilename);
     558             :         }
     559          51 :         return osRet;
     560          18 :     };
     561             : 
     562          36 :     const auto osFirstItemName(BuildVSICurlFilename(items.front().osFilename));
     563             :     auto poItemDS = std::unique_ptr<GDALDataset>(
     564          36 :         GDALDataset::Open(osFirstItemName.c_str()));
     565          18 :     if (!poItemDS)
     566             :     {
     567           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     568             :                  "Cannot open %s to retrieve band characteristics",
     569             :                  osFirstItemName.c_str());
     570           0 :         return false;
     571             :     }
     572             : 
     573             :     // Sort by ascending datetime
     574          18 :     std::sort(items.begin(), items.end(),
     575          26 :               [](const AssetItem &a, const AssetItem &b)
     576             :               {
     577          26 :                   if (!a.osDateTime.empty() && !b.osDateTime.empty())
     578          26 :                       return a.osDateTime < b.osDateTime;
     579           0 :                   return &a < &b;
     580             :               });
     581             : 
     582             :     // Create VRT bands and add sources
     583          18 :     bool bAtLeastOneBandHasNoData = false;
     584          36 :     for (int i = 0; i < poItemDS->GetRasterCount(); i++)
     585             :     {
     586          18 :         auto poItemBand = poItemDS->GetRasterBand(i + 1);
     587          18 :         AddBand(poItemBand->GetRasterDataType(), nullptr);
     588             :         auto poVRTBand =
     589          18 :             cpl::down_cast<VRTSourcedRasterBand *>(GetRasterBand(i + 1));
     590          18 :         int bHasNoData = FALSE;
     591          18 :         const double dfNoData = poItemBand->GetNoDataValue(&bHasNoData);
     592          18 :         if (bHasNoData)
     593             :         {
     594           4 :             bAtLeastOneBandHasNoData = true;
     595           4 :             poVRTBand->SetNoDataValue(dfNoData);
     596             :         }
     597             : 
     598          18 :         const auto eInterp = poItemBand->GetColorInterpretation();
     599          18 :         if (eInterp != GCI_Undefined)
     600          18 :             poVRTBand->SetColorInterpretation(eInterp);
     601             : 
     602             :         // Set band properties
     603          36 :         if (asset.bands.IsValid() &&
     604          18 :             asset.bands.Size() == poItemDS->GetRasterCount())
     605             :         {
     606          36 :             const auto &band = asset.bands[i];
     607          54 :             const auto osBandName = band["name"].ToString();
     608          18 :             if (!osBandName.empty())
     609          18 :                 poVRTBand->SetDescription(osBandName.c_str());
     610             : 
     611          54 :             auto osCommonName = band["eo:common_name"].ToString();
     612          18 :             if (osCommonName.empty())
     613          17 :                 osCommonName = band["common_name"].ToString();
     614          18 :             if (!osCommonName.empty())
     615             :             {
     616             :                 const auto eInterpFromCommonName =
     617          14 :                     GDALGetColorInterpFromSTACCommonName(osCommonName.c_str());
     618          14 :                 if (eInterpFromCommonName != GCI_Undefined)
     619          14 :                     poVRTBand->SetColorInterpretation(eInterpFromCommonName);
     620             :             }
     621             : 
     622          78 :             for (const auto &bandChild : band.GetChildren())
     623             :             {
     624         120 :                 const auto osChildName = bandChild.GetName();
     625          89 :                 if (osChildName != "name" && osChildName != "common_name" &&
     626          29 :                     osChildName != "eo:common_name")
     627             :                 {
     628          28 :                     poVRTBand->SetMetadataItem(osChildName.c_str(),
     629          56 :                                                bandChild.ToString().c_str());
     630             :                 }
     631             :             }
     632             :         }
     633             : 
     634             :         // Add items as VRT sources
     635          51 :         for (const auto &assetItem : items)
     636             :         {
     637          66 :             const auto osItemName(BuildVSICurlFilename(assetItem.osFilename));
     638          33 :             const double dfDstXOff = (assetItem.dfXMin - dfXMin) / dfXRes;
     639          33 :             const double dfDstXSize =
     640          33 :                 (assetItem.dfXMax - assetItem.dfXMin) / dfXRes;
     641          33 :             const double dfDstYOff = (dfYMax - assetItem.dfYMax) / dfYRes;
     642          33 :             const double dfDstYSize =
     643          33 :                 (assetItem.dfYMax - assetItem.dfYMin) / dfYRes;
     644          33 :             if (!bHasNoData)
     645             :             {
     646          25 :                 poVRTBand->AddSimpleSource(osItemName.c_str(), i + 1, 0, 0,
     647          25 :                                            assetItem.nXSize, assetItem.nYSize,
     648             :                                            dfDstXOff, dfDstYOff, dfDstXSize,
     649             :                                            dfDstYSize);
     650             :             }
     651             :             else
     652             :             {
     653           8 :                 poVRTBand->AddComplexSource(osItemName.c_str(), i + 1, 0, 0,
     654           8 :                                             assetItem.nXSize, assetItem.nYSize,
     655             :                                             dfDstXOff, dfDstYOff, dfDstXSize,
     656             :                                             dfDstYSize,
     657             :                                             0.0,  // offset
     658             :                                             1.0,  // scale
     659             :                                             dfNoData);
     660             :             }
     661             :         }
     662             : 
     663             :         const char *pszOverlapStrategy =
     664          18 :             CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
     665             :                                  "OVERLAP_STRATEGY", "REMOVE_IF_NO_NODATA");
     666          18 :         if ((EQUAL(pszOverlapStrategy, "REMOVE_IF_NO_NODATA") &&
     667          14 :              !bAtLeastOneBandHasNoData) ||
     668           6 :             EQUAL(pszOverlapStrategy, "USE_MOST_RECENT"))
     669             :         {
     670          14 :             const char *const apszOptions[] = {
     671             :                 "EMIT_ERROR_IF_GEOS_NOT_AVAILABLE=NO", nullptr};
     672          14 :             poVRTBand->RemoveCoveredSources(apszOptions);
     673             :         }
     674             :     }
     675          18 :     return true;
     676             : }
     677             : 
     678             : /************************************************************************/
     679             : /*                         SetSubdatasets()                             */
     680             : /************************************************************************/
     681             : 
     682           1 : void STACITDataset::SetSubdatasets(
     683             :     const std::string &osFilename,
     684             :     const std::map<std::string, Collection> &oMapCollection)
     685             : {
     686           2 :     CPLStringList aosSubdatasets;
     687           1 :     int nCount = 1;
     688           3 :     for (const auto &collectionKV : oMapCollection)
     689             :     {
     690           5 :         for (const auto &assetKV : collectionKV.second.assets)
     691             :         {
     692           6 :             std::string osCollectionAssetArg;
     693           3 :             if (oMapCollection.size() > 1)
     694             :                 osCollectionAssetArg +=
     695           3 :                     "collection=" + collectionKV.first + ",";
     696           3 :             osCollectionAssetArg += "asset=" + assetKV.first;
     697             : 
     698           6 :             std::string osCollectionAssetText;
     699           3 :             if (oMapCollection.size() > 1)
     700             :                 osCollectionAssetText +=
     701           3 :                     "Collection " + collectionKV.first + ", ";
     702           3 :             osCollectionAssetText += "Asset " + assetKV.first;
     703             : 
     704           3 :             if (assetKV.second.assets.size() == 1)
     705             :             {
     706             :                 aosSubdatasets.AddString(CPLSPrintf(
     707             :                     "SUBDATASET_%d_NAME=STACIT:\"%s\":%s", nCount,
     708           2 :                     osFilename.c_str(), osCollectionAssetArg.c_str()));
     709             :                 aosSubdatasets.AddString(CPLSPrintf(
     710             :                     "SUBDATASET_%d_DESC=%s of %s", nCount,
     711           2 :                     osCollectionAssetText.c_str(), osFilename.c_str()));
     712           2 :                 nCount++;
     713             :             }
     714             :             else
     715             :             {
     716           3 :                 for (const auto &assetsByProjKV : assetKV.second.assets)
     717             :                 {
     718             :                     aosSubdatasets.AddString(CPLSPrintf(
     719             :                         "SUBDATASET_%d_NAME=STACIT:\"%s\":%s,crs=%s", nCount,
     720             :                         osFilename.c_str(), osCollectionAssetArg.c_str(),
     721           2 :                         SanitizeCRSValue(assetsByProjKV.first).c_str()));
     722             :                     aosSubdatasets.AddString(CPLSPrintf(
     723             :                         "SUBDATASET_%d_DESC=%s of %s in CRS %s", nCount,
     724             :                         osCollectionAssetText.c_str(), osFilename.c_str(),
     725           2 :                         assetsByProjKV.first.c_str()));
     726           2 :                     nCount++;
     727             :                 }
     728             :             }
     729             :         }
     730             :     }
     731           1 :     GDALDataset::SetMetadata(aosSubdatasets.List(), "SUBDATASETS");
     732           1 : }
     733             : 
     734             : /************************************************************************/
     735             : /*                               Open()                                 */
     736             : /************************************************************************/
     737             : 
     738          21 : bool STACITDataset::Open(GDALOpenInfo *poOpenInfo)
     739             : {
     740          42 :     CPLString osFilename(poOpenInfo->pszFilename);
     741             :     std::string osFilteredCollection =
     742          42 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "COLLECTION", "");
     743             :     std::string osFilteredAsset =
     744          42 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ASSET", "");
     745             :     std::string osFilteredCRS = SanitizeCRSValue(
     746          63 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", ""));
     747          21 :     if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
     748             :     {
     749             :         const CPLStringList aosTokens(CSLTokenizeString2(
     750           6 :             poOpenInfo->pszFilename, ":", CSLT_HONOURSTRINGS));
     751           6 :         if (aosTokens.size() != 2 && aosTokens.size() != 3)
     752           0 :             return false;
     753           6 :         osFilename = aosTokens[1];
     754           6 :         if (aosTokens.size() >= 3)
     755             :         {
     756             :             const CPLStringList aosFilters(
     757          12 :                 CSLTokenizeString2(aosTokens[2], ",", 0));
     758             :             osFilteredCollection = aosFilters.FetchNameValueDef(
     759           6 :                 "collection", osFilteredCollection.c_str());
     760             :             osFilteredAsset =
     761           6 :                 aosFilters.FetchNameValueDef("asset", osFilteredAsset.c_str());
     762             :             osFilteredCRS =
     763           6 :                 aosFilters.FetchNameValueDef("crs", osFilteredCRS.c_str());
     764             :         }
     765             :     }
     766             : 
     767          42 :     std::map<std::string, Collection> oMapCollection;
     768          21 :     GIntBig nItemIter = 0;
     769          21 :     GIntBig nMaxItems = CPLAtoGIntBig(CSLFetchNameValueDef(
     770          21 :         poOpenInfo->papszOpenOptions, "MAX_ITEMS", "1000"));
     771             : 
     772             :     const bool bMaxItemsSpecified =
     773          21 :         CSLFetchNameValue(poOpenInfo->papszOpenOptions, "MAX_ITEMS") != nullptr;
     774          21 :     if (!bMaxItemsSpecified)
     775             :     {
     776             :         // If the URL includes a limit parameter, and it's larger than our
     777             :         // default MAX_ITEMS value, then increase the later to the former.
     778          20 :         const auto nPos = osFilename.ifind("&limit=");
     779          20 :         if (nPos != std::string::npos)
     780             :         {
     781           0 :             const auto nLimit = CPLAtoGIntBig(
     782           0 :                 osFilename.substr(nPos + strlen("&limit=")).c_str());
     783           0 :             nMaxItems = std::max(nMaxItems, nLimit);
     784             :         }
     785             :     }
     786             : 
     787          42 :     auto osCurFilename = osFilename;
     788          42 :     std::string osMethod = "GET";
     789          42 :     CPLJSONObject oHeaders;
     790          42 :     CPLJSONObject oBody;
     791          21 :     bool bMerge = false;
     792          21 :     int nLoops = 0;
     793           3 :     do
     794             :     {
     795          24 :         ++nLoops;
     796          24 :         if (nMaxItems > 0 && nLoops > nMaxItems)
     797             :         {
     798          21 :             break;
     799             :         }
     800             : 
     801          24 :         CPLJSONDocument oDoc;
     802             : 
     803          47 :         if (STARTS_WITH(osCurFilename, "http://") ||
     804          23 :             STARTS_WITH(osCurFilename, "https://"))
     805             :         {
     806             :             // Cf // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
     807           1 :             CPLStringList aosOptions;
     808           2 :             if (oBody.IsValid() &&
     809           1 :                 oBody.GetType() == CPLJSONObject::Type::Object)
     810             :             {
     811           1 :                 if (bMerge)
     812           0 :                     CPLDebug("STACIT",
     813             :                              "Ignoring 'merge' attribute from next link");
     814             :                 const std::string osPostContent =
     815           2 :                     oBody.Format(CPLJSONObject::PrettyFormat::Pretty);
     816           1 :                 aosOptions.SetNameValue("POSTFIELDS", osPostContent.c_str());
     817             :             }
     818           1 :             aosOptions.SetNameValue("CUSTOMREQUEST", osMethod.c_str());
     819           1 :             CPLString osHeaders;
     820           4 :             if (!oHeaders.IsValid() ||
     821           2 :                 oHeaders.GetType() != CPLJSONObject::Type::Object ||
     822           2 :                 oHeaders["Content-Type"].ToString().empty())
     823             :             {
     824           1 :                 osHeaders = "Content-Type: application/json";
     825             :             }
     826           2 :             if (oHeaders.IsValid() &&
     827           1 :                 oHeaders.GetType() == CPLJSONObject::Type::Object)
     828             :             {
     829           2 :                 for (const auto &obj : oHeaders.GetChildren())
     830             :                 {
     831           1 :                     osHeaders += "\r\n";
     832           1 :                     osHeaders += obj.GetName();
     833           1 :                     osHeaders += ": ";
     834           1 :                     osHeaders += obj.ToString();
     835             :                 }
     836             :             }
     837           1 :             aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
     838             :             CPLHTTPResult *psResult =
     839           1 :                 CPLHTTPFetch(osCurFilename.c_str(), aosOptions.List());
     840           1 :             if (!psResult)
     841           0 :                 return false;
     842           1 :             if (!psResult->pabyData)
     843             :             {
     844           0 :                 CPLHTTPDestroyResult(psResult);
     845           0 :                 return false;
     846             :             }
     847           2 :             const bool bOK = oDoc.LoadMemory(
     848           1 :                 reinterpret_cast<const char *>(psResult->pabyData));
     849             :             // CPLDebug("STACIT", "Response: %s", reinterpret_cast<const char*>(psResult->pabyData));
     850           1 :             CPLHTTPDestroyResult(psResult);
     851           1 :             if (!bOK)
     852           0 :                 return false;
     853             :         }
     854             :         else
     855             :         {
     856          23 :             if (!oDoc.Load(osCurFilename))
     857           0 :                 return false;
     858             :         }
     859          24 :         const auto oRoot = oDoc.GetRoot();
     860          48 :         auto oFeatures = oRoot.GetArray("features");
     861          24 :         if (!oFeatures.IsValid())
     862             :         {
     863           2 :             if (oRoot.GetString("type") == "Feature")
     864             :             {
     865           2 :                 oFeatures = CPLJSONArray();
     866           2 :                 oFeatures.Add(oRoot);
     867             :             }
     868             :             else
     869             :             {
     870           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Missing features");
     871           0 :                 return false;
     872             :             }
     873             :         }
     874          74 :         for (const auto &oFeature : oFeatures)
     875             :         {
     876          50 :             nItemIter++;
     877          50 :             if (nMaxItems > 0 && nItemIter > nMaxItems)
     878             :             {
     879           0 :                 break;
     880             :             }
     881             : 
     882         100 :             auto oStacExtensions = oFeature.GetArray("stac_extensions");
     883          50 :             if (!oStacExtensions.IsValid())
     884             :             {
     885           0 :                 CPLDebug("STACIT",
     886             :                          "Skipping Feature that lacks stac_extensions");
     887           0 :                 continue;
     888             :             }
     889          50 :             bool bProjExtensionFound = false;
     890         150 :             for (const auto &oStacExtension : oStacExtensions)
     891             :             {
     892         251 :                 if (oStacExtension.ToString() == "proj" ||
     893         151 :                     oStacExtension.ToString().find(
     894             :                         "https://stac-extensions.github.io/projection/") == 0)
     895             :                 {
     896          50 :                     bProjExtensionFound = true;
     897          50 :                     break;
     898             :                 }
     899             :             }
     900          50 :             if (!bProjExtensionFound)
     901             :             {
     902           0 :                 CPLDebug(
     903             :                     "STACIT",
     904             :                     "Skipping Feature that lacks the 'proj' STAC extension");
     905           0 :                 continue;
     906             :             }
     907             : 
     908         100 :             auto jAssets = oFeature["assets"];
     909         100 :             if (!jAssets.IsValid() ||
     910          50 :                 jAssets.GetType() != CPLJSONObject::Type::Object)
     911             :             {
     912           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     913             :                          "Missing assets on a Feature");
     914           0 :                 continue;
     915             :             }
     916             : 
     917         100 :             auto oProperties = oFeature["properties"];
     918         100 :             if (!oProperties.IsValid() ||
     919          50 :                 oProperties.GetType() != CPLJSONObject::Type::Object)
     920             :             {
     921           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     922             :                          "Missing properties on a Feature");
     923           0 :                 continue;
     924             :             }
     925             : 
     926         100 :             const auto osCollection = oFeature["collection"].ToString();
     927          65 :             if (!osFilteredCollection.empty() &&
     928          15 :                 osFilteredCollection != osCollection)
     929           8 :                 continue;
     930             : 
     931          95 :             for (const auto &jAsset : jAssets.GetChildren())
     932             :             {
     933          53 :                 const auto osAssetName = jAsset.GetName();
     934          53 :                 if (!osFilteredAsset.empty() && osFilteredAsset != osAssetName)
     935           8 :                     continue;
     936             : 
     937          45 :                 ParseAsset(jAsset, oProperties, osCollection, osFilteredCRS,
     938             :                            oMapCollection);
     939             :             }
     940             :         }
     941          24 :         if (nMaxItems > 0 && nItemIter >= nMaxItems)
     942             :         {
     943           1 :             if (!bMaxItemsSpecified)
     944             :             {
     945           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     946             :                          "Maximum number of items (" CPL_FRMT_GIB
     947             :                          ") allowed to be retrieved has been hit",
     948             :                          nMaxItems);
     949             :             }
     950             :             else
     951             :             {
     952           1 :                 CPLDebug("STACIT",
     953             :                          "Maximum number of items (" CPL_FRMT_GIB
     954             :                          ") allowed to be retrieved has been hit",
     955             :                          nMaxItems);
     956             :             }
     957           1 :             break;
     958             :         }
     959             : 
     960             :         // Follow next link
     961             :         // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
     962          46 :         const auto oLinks = oRoot.GetArray("links");
     963          23 :         if (!oLinks.IsValid())
     964          20 :             break;
     965           6 :         std::string osNewFilename;
     966           6 :         for (const auto &oLink : oLinks)
     967             :         {
     968           6 :             const auto osType = oLink["type"].ToString();
     969           6 :             if (oLink["rel"].ToString() == "next" &&
     970           3 :                 (osType.empty() || osType == "application/geo+json"))
     971             :             {
     972           3 :                 osMethod = oLink.GetString("method", "GET");
     973           3 :                 osNewFilename = oLink["href"].ToString();
     974           3 :                 oHeaders = oLink["headers"];
     975           3 :                 oBody = oLink["body"];
     976           3 :                 bMerge = oLink.GetBool("merge", false);
     977           3 :                 if (osType == "application/geo+json")
     978             :                 {
     979           0 :                     break;
     980             :                 }
     981             :             }
     982             :         }
     983           6 :         if (!osNewFilename.empty() &&
     984           3 :             (osNewFilename != osCurFilename ||
     985           0 :              (oBody.IsValid() &&
     986           0 :               oBody.GetType() == CPLJSONObject::Type::Object)))
     987             :         {
     988           3 :             osCurFilename = osNewFilename;
     989             :         }
     990             :         else
     991           0 :             osCurFilename.clear();
     992           3 :     } while (!osCurFilename.empty());
     993             : 
     994          21 :     if (oMapCollection.empty())
     995             :     {
     996           2 :         CPLError(CE_Failure, CPLE_AppDefined, "No compatible asset found");
     997           2 :         return false;
     998             :     }
     999             : 
    1000          37 :     if (oMapCollection.size() > 1 ||
    1001          37 :         oMapCollection.begin()->second.assets.size() > 1 ||
    1002          37 :         oMapCollection.begin()->second.assets.begin()->second.assets.size() > 1)
    1003             :     {
    1004             :         // If there's more than one asset type or more than one SRS, expose
    1005             :         // subdatasets.
    1006           1 :         SetSubdatasets(osFilename, oMapCollection);
    1007           1 :         return true;
    1008             :     }
    1009             :     else
    1010             :     {
    1011          18 :         return SetupDataset(poOpenInfo, osFilename, oMapCollection);
    1012             :     }
    1013             : }
    1014             : 
    1015             : /************************************************************************/
    1016             : /*                            OpenStatic()                              */
    1017             : /************************************************************************/
    1018             : 
    1019          21 : GDALDataset *STACITDataset::OpenStatic(GDALOpenInfo *poOpenInfo)
    1020             : {
    1021          21 :     if (!Identify(poOpenInfo))
    1022           0 :         return nullptr;
    1023          42 :     auto poDS = std::make_unique<STACITDataset>();
    1024          21 :     if (!poDS->Open(poOpenInfo))
    1025           2 :         return nullptr;
    1026          19 :     return poDS.release();
    1027             : }
    1028             : 
    1029             : /************************************************************************/
    1030             : /*                       GDALRegister_STACIT()                          */
    1031             : /************************************************************************/
    1032             : 
    1033        2033 : void GDALRegister_STACIT()
    1034             : 
    1035             : {
    1036        2033 :     if (GDALGetDriverByName("STACIT") != nullptr)
    1037         283 :         return;
    1038             : 
    1039        1750 :     GDALDriver *poDriver = new GDALDriver();
    1040             : 
    1041        1750 :     poDriver->SetDescription("STACIT");
    1042        1750 :     poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    1043        1750 :     poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
    1044        1750 :                               "Spatio-Temporal Asset Catalog Items");
    1045        1750 :     poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/stacit.html");
    1046        1750 :     poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
    1047        1750 :     poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
    1048        1750 :     poDriver->SetMetadataItem(
    1049             :         GDAL_DMD_OPENOPTIONLIST,
    1050             :         "<OpenOptionList>"
    1051             :         "   <Option name='MAX_ITEMS' type='int' default='1000' "
    1052             :         "description='Maximum number of items fetched. 0=unlimited'/>"
    1053             :         "   <Option name='COLLECTION' type='string' "
    1054             :         "description='Name of collection to filter items'/>"
    1055             :         "   <Option name='ASSET' type='string' "
    1056             :         "description='Name of asset to filter items'/>"
    1057             :         "   <Option name='CRS' type='string' "
    1058             :         "description='Name of CRS to filter items'/>"
    1059             :         "   <Option name='RESOLUTION' type='string-select' default='AVERAGE' "
    1060             :         "description='Strategy to use to determine dataset resolution'>"
    1061             :         "       <Value>AVERAGE</Value>"
    1062             :         "       <Value>HIGHEST</Value>"
    1063             :         "       <Value>LOWEST</Value>"
    1064             :         "   </Option>"
    1065             :         "   <Option name='OVERLAP_STRATEGY' type='string-select' "
    1066             :         "default='REMOVE_IF_NO_NODATA' "
    1067             :         "description='Strategy to use when some sources are fully "
    1068             :         "covered by others'>"
    1069             :         "       <Value>REMOVE_IF_NO_NODATA</Value>"
    1070             :         "       <Value>USE_ALL</Value>"
    1071             :         "       <Value>USE_MOST_RECENT</Value>"
    1072             :         "   </Option>"
    1073        1750 :         "</OpenOptionList>");
    1074             : 
    1075        1750 :     poDriver->pfnOpen = STACITDataset::OpenStatic;
    1076        1750 :     poDriver->pfnIdentify = STACITDataset::Identify;
    1077             : 
    1078        1750 :     GetGDALDriverManager()->RegisterDriver(poDriver);
    1079             : }

Generated by: LCOV version 1.14