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

Generated by: LCOV version 1.14