LCOV - code coverage report
Current view: top level - frmts/stacit - stacitdataset.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 422 500 84.4 %
Date: 2025-09-10 17:48:50 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       57193 : int STACITDataset::Identify(GDALOpenInfo *poOpenInfo)
      97             : {
      98       57193 :     if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
      99             :     {
     100          12 :         return true;
     101             :     }
     102             : 
     103       57181 :     const bool bIsSingleDriver = poOpenInfo->IsSingleAllowedDriver("STACIT");
     104       57178 :     if (bIsSingleDriver && (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
     105           4 :                             STARTS_WITH(poOpenInfo->pszFilename, "https://")))
     106             :     {
     107           1 :         return true;
     108             :     }
     109             : 
     110       57177 :     if (poOpenInfo->nHeaderBytes == 0)
     111             :     {
     112       54042 :         return false;
     113             :     }
     114             : 
     115        9335 :     for (int i = 0; i < 2; i++)
     116             :     {
     117             :         // TryToIngest() may reallocate pabyHeader, so do not move this
     118             :         // before the loop.
     119        6234 :         const char *pszHeader =
     120             :             reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
     121        6630 :         while (*pszHeader != 0 &&
     122        4794 :                std::isspace(static_cast<unsigned char>(*pszHeader)))
     123         396 :             ++pszHeader;
     124        6234 :         if (bIsSingleDriver)
     125             :         {
     126           4 :             return pszHeader[0] == '{';
     127             :         }
     128             : 
     129        6230 :         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        6202 :         if (i == 0)
     145             :         {
     146             :             // Should be enough for a STACIT .json file
     147        3101 :             poOpenInfo->TryToIngest(32768);
     148             :         }
     149             :     }
     150             : 
     151        3101 :     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             :         else
     548             :         {
     549          51 :             osRet = VSIURIToVSIPath(osFilename);
     550             :         }
     551          51 :         return osRet;
     552          18 :     };
     553             : 
     554          36 :     const auto osFirstItemName(BuildVSICurlFilename(items.front().osFilename));
     555             :     auto poItemDS = std::unique_ptr<GDALDataset>(
     556          36 :         GDALDataset::Open(osFirstItemName.c_str()));
     557          18 :     if (!poItemDS)
     558             :     {
     559           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     560             :                  "Cannot open %s to retrieve band characteristics",
     561             :                  osFirstItemName.c_str());
     562           0 :         return false;
     563             :     }
     564             : 
     565             :     // Sort by ascending datetime
     566          18 :     std::sort(items.begin(), items.end(),
     567          26 :               [](const AssetItem &a, const AssetItem &b)
     568             :               {
     569          26 :                   if (!a.osDateTime.empty() && !b.osDateTime.empty())
     570          26 :                       return a.osDateTime < b.osDateTime;
     571           0 :                   return &a < &b;
     572             :               });
     573             : 
     574             :     // Create VRT bands and add sources
     575          18 :     bool bAtLeastOneBandHasNoData = false;
     576          36 :     for (int i = 0; i < poItemDS->GetRasterCount(); i++)
     577             :     {
     578          18 :         auto poItemBand = poItemDS->GetRasterBand(i + 1);
     579          18 :         AddBand(poItemBand->GetRasterDataType(), nullptr);
     580             :         auto poVRTBand =
     581          18 :             cpl::down_cast<VRTSourcedRasterBand *>(GetRasterBand(i + 1));
     582          18 :         int bHasNoData = FALSE;
     583          18 :         const double dfNoData = poItemBand->GetNoDataValue(&bHasNoData);
     584          18 :         if (bHasNoData)
     585             :         {
     586           4 :             bAtLeastOneBandHasNoData = true;
     587           4 :             poVRTBand->SetNoDataValue(dfNoData);
     588             :         }
     589             : 
     590          18 :         const auto eInterp = poItemBand->GetColorInterpretation();
     591          18 :         if (eInterp != GCI_Undefined)
     592          18 :             poVRTBand->SetColorInterpretation(eInterp);
     593             : 
     594             :         // Set band properties
     595          36 :         if (asset.bands.IsValid() &&
     596          18 :             asset.bands.Size() == poItemDS->GetRasterCount())
     597             :         {
     598          36 :             const auto &band = asset.bands[i];
     599          54 :             const auto osBandName = band["name"].ToString();
     600          18 :             if (!osBandName.empty())
     601          18 :                 poVRTBand->SetDescription(osBandName.c_str());
     602             : 
     603          54 :             auto osCommonName = band["eo:common_name"].ToString();
     604          18 :             if (osCommonName.empty())
     605          17 :                 osCommonName = band["common_name"].ToString();
     606          18 :             if (!osCommonName.empty())
     607             :             {
     608             :                 const auto eInterpFromCommonName =
     609          14 :                     GDALGetColorInterpFromSTACCommonName(osCommonName.c_str());
     610          14 :                 if (eInterpFromCommonName != GCI_Undefined)
     611          14 :                     poVRTBand->SetColorInterpretation(eInterpFromCommonName);
     612             :             }
     613             : 
     614          78 :             for (const auto &bandChild : band.GetChildren())
     615             :             {
     616         120 :                 const auto osChildName = bandChild.GetName();
     617          89 :                 if (osChildName != "name" && osChildName != "common_name" &&
     618          29 :                     osChildName != "eo:common_name")
     619             :                 {
     620          28 :                     poVRTBand->SetMetadataItem(osChildName.c_str(),
     621          56 :                                                bandChild.ToString().c_str());
     622             :                 }
     623             :             }
     624             :         }
     625             : 
     626             :         // Add items as VRT sources
     627          51 :         for (const auto &assetItem : items)
     628             :         {
     629          66 :             const auto osItemName(BuildVSICurlFilename(assetItem.osFilename));
     630          33 :             const double dfDstXOff = (assetItem.dfXMin - dfXMin) / dfXRes;
     631          33 :             const double dfDstXSize =
     632          33 :                 (assetItem.dfXMax - assetItem.dfXMin) / dfXRes;
     633          33 :             const double dfDstYOff = (dfYMax - assetItem.dfYMax) / dfYRes;
     634          33 :             const double dfDstYSize =
     635          33 :                 (assetItem.dfYMax - assetItem.dfYMin) / dfYRes;
     636          33 :             if (!bHasNoData)
     637             :             {
     638          25 :                 poVRTBand->AddSimpleSource(osItemName.c_str(), i + 1, 0, 0,
     639          25 :                                            assetItem.nXSize, assetItem.nYSize,
     640             :                                            dfDstXOff, dfDstYOff, dfDstXSize,
     641             :                                            dfDstYSize);
     642             :             }
     643             :             else
     644             :             {
     645           8 :                 poVRTBand->AddComplexSource(osItemName.c_str(), i + 1, 0, 0,
     646           8 :                                             assetItem.nXSize, assetItem.nYSize,
     647             :                                             dfDstXOff, dfDstYOff, dfDstXSize,
     648             :                                             dfDstYSize,
     649             :                                             0.0,  // offset
     650             :                                             1.0,  // scale
     651             :                                             dfNoData);
     652             :             }
     653             :         }
     654             : 
     655             :         const char *pszOverlapStrategy =
     656          18 :             CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
     657             :                                  "OVERLAP_STRATEGY", "REMOVE_IF_NO_NODATA");
     658          18 :         if ((EQUAL(pszOverlapStrategy, "REMOVE_IF_NO_NODATA") &&
     659          14 :              !bAtLeastOneBandHasNoData) ||
     660           6 :             EQUAL(pszOverlapStrategy, "USE_MOST_RECENT"))
     661             :         {
     662          14 :             const char *const apszOptions[] = {
     663             :                 "EMIT_ERROR_IF_GEOS_NOT_AVAILABLE=NO", nullptr};
     664          14 :             poVRTBand->RemoveCoveredSources(apszOptions);
     665             :         }
     666             :     }
     667          18 :     return true;
     668             : }
     669             : 
     670             : /************************************************************************/
     671             : /*                         SetSubdatasets()                             */
     672             : /************************************************************************/
     673             : 
     674           1 : void STACITDataset::SetSubdatasets(
     675             :     const std::string &osFilename,
     676             :     const std::map<std::string, Collection> &oMapCollection)
     677             : {
     678           2 :     CPLStringList aosSubdatasets;
     679           1 :     int nCount = 1;
     680           3 :     for (const auto &collectionKV : oMapCollection)
     681             :     {
     682           5 :         for (const auto &assetKV : collectionKV.second.assets)
     683             :         {
     684           6 :             std::string osCollectionAssetArg;
     685           3 :             if (oMapCollection.size() > 1)
     686             :                 osCollectionAssetArg +=
     687           3 :                     "collection=" + collectionKV.first + ",";
     688           3 :             osCollectionAssetArg += "asset=" + assetKV.first;
     689             : 
     690           6 :             std::string osCollectionAssetText;
     691           3 :             if (oMapCollection.size() > 1)
     692             :                 osCollectionAssetText +=
     693           3 :                     "Collection " + collectionKV.first + ", ";
     694           3 :             osCollectionAssetText += "Asset " + assetKV.first;
     695             : 
     696           3 :             if (assetKV.second.assets.size() == 1)
     697             :             {
     698             :                 aosSubdatasets.AddString(CPLSPrintf(
     699             :                     "SUBDATASET_%d_NAME=STACIT:\"%s\":%s", nCount,
     700           2 :                     osFilename.c_str(), osCollectionAssetArg.c_str()));
     701             :                 aosSubdatasets.AddString(CPLSPrintf(
     702             :                     "SUBDATASET_%d_DESC=%s of %s", nCount,
     703           2 :                     osCollectionAssetText.c_str(), osFilename.c_str()));
     704           2 :                 nCount++;
     705             :             }
     706             :             else
     707             :             {
     708           3 :                 for (const auto &assetsByProjKV : assetKV.second.assets)
     709             :                 {
     710             :                     aosSubdatasets.AddString(CPLSPrintf(
     711             :                         "SUBDATASET_%d_NAME=STACIT:\"%s\":%s,crs=%s", nCount,
     712             :                         osFilename.c_str(), osCollectionAssetArg.c_str(),
     713           2 :                         SanitizeCRSValue(assetsByProjKV.first).c_str()));
     714             :                     aosSubdatasets.AddString(CPLSPrintf(
     715             :                         "SUBDATASET_%d_DESC=%s of %s in CRS %s", nCount,
     716             :                         osCollectionAssetText.c_str(), osFilename.c_str(),
     717           2 :                         assetsByProjKV.first.c_str()));
     718           2 :                     nCount++;
     719             :                 }
     720             :             }
     721             :         }
     722             :     }
     723           1 :     GDALDataset::SetMetadata(aosSubdatasets.List(), "SUBDATASETS");
     724           1 : }
     725             : 
     726             : /************************************************************************/
     727             : /*                               Open()                                 */
     728             : /************************************************************************/
     729             : 
     730          21 : bool STACITDataset::Open(GDALOpenInfo *poOpenInfo)
     731             : {
     732          42 :     CPLString osFilename(poOpenInfo->pszFilename);
     733             :     std::string osFilteredCollection =
     734          42 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "COLLECTION", "");
     735             :     std::string osFilteredAsset =
     736          42 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ASSET", "");
     737             :     std::string osFilteredCRS = SanitizeCRSValue(
     738          63 :         CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", ""));
     739          21 :     if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
     740             :     {
     741             :         const CPLStringList aosTokens(CSLTokenizeString2(
     742           6 :             poOpenInfo->pszFilename, ":", CSLT_HONOURSTRINGS));
     743           6 :         if (aosTokens.size() != 2 && aosTokens.size() != 3)
     744           0 :             return false;
     745           6 :         osFilename = aosTokens[1];
     746           6 :         if (aosTokens.size() >= 3)
     747             :         {
     748             :             const CPLStringList aosFilters(
     749          12 :                 CSLTokenizeString2(aosTokens[2], ",", 0));
     750             :             osFilteredCollection = aosFilters.FetchNameValueDef(
     751           6 :                 "collection", osFilteredCollection.c_str());
     752             :             osFilteredAsset =
     753           6 :                 aosFilters.FetchNameValueDef("asset", osFilteredAsset.c_str());
     754             :             osFilteredCRS =
     755           6 :                 aosFilters.FetchNameValueDef("crs", osFilteredCRS.c_str());
     756             :         }
     757             :     }
     758             : 
     759          42 :     std::map<std::string, Collection> oMapCollection;
     760          21 :     GIntBig nItemIter = 0;
     761          21 :     GIntBig nMaxItems = CPLAtoGIntBig(CSLFetchNameValueDef(
     762          21 :         poOpenInfo->papszOpenOptions, "MAX_ITEMS", "1000"));
     763             : 
     764             :     const bool bMaxItemsSpecified =
     765          21 :         CSLFetchNameValue(poOpenInfo->papszOpenOptions, "MAX_ITEMS") != nullptr;
     766          21 :     if (!bMaxItemsSpecified)
     767             :     {
     768             :         // If the URL includes a limit parameter, and it's larger than our
     769             :         // default MAX_ITEMS value, then increase the later to the former.
     770          20 :         const auto nPos = osFilename.ifind("&limit=");
     771          20 :         if (nPos != std::string::npos)
     772             :         {
     773           0 :             const auto nLimit = CPLAtoGIntBig(
     774           0 :                 osFilename.substr(nPos + strlen("&limit=")).c_str());
     775           0 :             nMaxItems = std::max(nMaxItems, nLimit);
     776             :         }
     777             :     }
     778             : 
     779          42 :     auto osCurFilename = osFilename;
     780          42 :     std::string osMethod = "GET";
     781          42 :     CPLJSONObject oHeaders;
     782          42 :     CPLJSONObject oBody;
     783          21 :     bool bMerge = false;
     784          21 :     int nLoops = 0;
     785           3 :     do
     786             :     {
     787          24 :         ++nLoops;
     788          24 :         if (nMaxItems > 0 && nLoops > nMaxItems)
     789             :         {
     790          21 :             break;
     791             :         }
     792             : 
     793          24 :         CPLJSONDocument oDoc;
     794             : 
     795          47 :         if (STARTS_WITH(osCurFilename, "http://") ||
     796          23 :             STARTS_WITH(osCurFilename, "https://"))
     797             :         {
     798             :             // Cf // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
     799           1 :             CPLStringList aosOptions;
     800           2 :             if (oBody.IsValid() &&
     801           1 :                 oBody.GetType() == CPLJSONObject::Type::Object)
     802             :             {
     803           1 :                 if (bMerge)
     804           0 :                     CPLDebug("STACIT",
     805             :                              "Ignoring 'merge' attribute from next link");
     806             :                 const std::string osPostContent =
     807           2 :                     oBody.Format(CPLJSONObject::PrettyFormat::Pretty);
     808           1 :                 aosOptions.SetNameValue("POSTFIELDS", osPostContent.c_str());
     809             :             }
     810           1 :             aosOptions.SetNameValue("CUSTOMREQUEST", osMethod.c_str());
     811           1 :             CPLString osHeaders;
     812           4 :             if (!oHeaders.IsValid() ||
     813           2 :                 oHeaders.GetType() != CPLJSONObject::Type::Object ||
     814           2 :                 oHeaders["Content-Type"].ToString().empty())
     815             :             {
     816           1 :                 osHeaders = "Content-Type: application/json";
     817             :             }
     818           2 :             if (oHeaders.IsValid() &&
     819           1 :                 oHeaders.GetType() == CPLJSONObject::Type::Object)
     820             :             {
     821           2 :                 for (const auto &obj : oHeaders.GetChildren())
     822             :                 {
     823           1 :                     osHeaders += "\r\n";
     824           1 :                     osHeaders += obj.GetName();
     825           1 :                     osHeaders += ": ";
     826           1 :                     osHeaders += obj.ToString();
     827             :                 }
     828             :             }
     829           1 :             aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
     830             :             CPLHTTPResult *psResult =
     831           1 :                 CPLHTTPFetch(osCurFilename.c_str(), aosOptions.List());
     832           1 :             if (!psResult)
     833           0 :                 return false;
     834           1 :             if (!psResult->pabyData)
     835             :             {
     836           0 :                 CPLHTTPDestroyResult(psResult);
     837           0 :                 return false;
     838             :             }
     839           2 :             const bool bOK = oDoc.LoadMemory(
     840           1 :                 reinterpret_cast<const char *>(psResult->pabyData));
     841             :             // CPLDebug("STACIT", "Response: %s", reinterpret_cast<const char*>(psResult->pabyData));
     842           1 :             CPLHTTPDestroyResult(psResult);
     843           1 :             if (!bOK)
     844           0 :                 return false;
     845             :         }
     846             :         else
     847             :         {
     848          23 :             if (!oDoc.Load(osCurFilename))
     849           0 :                 return false;
     850             :         }
     851          24 :         const auto oRoot = oDoc.GetRoot();
     852          48 :         auto oFeatures = oRoot.GetArray("features");
     853          24 :         if (!oFeatures.IsValid())
     854             :         {
     855           2 :             if (oRoot.GetString("type") == "Feature")
     856             :             {
     857           2 :                 oFeatures = CPLJSONArray();
     858           2 :                 oFeatures.Add(oRoot);
     859             :             }
     860             :             else
     861             :             {
     862           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Missing features");
     863           0 :                 return false;
     864             :             }
     865             :         }
     866          74 :         for (const auto &oFeature : oFeatures)
     867             :         {
     868          50 :             nItemIter++;
     869          50 :             if (nMaxItems > 0 && nItemIter > nMaxItems)
     870             :             {
     871           0 :                 break;
     872             :             }
     873             : 
     874         100 :             auto oStacExtensions = oFeature.GetArray("stac_extensions");
     875          50 :             if (!oStacExtensions.IsValid())
     876             :             {
     877           0 :                 CPLDebug("STACIT",
     878             :                          "Skipping Feature that lacks stac_extensions");
     879           0 :                 continue;
     880             :             }
     881          50 :             bool bProjExtensionFound = false;
     882         150 :             for (const auto &oStacExtension : oStacExtensions)
     883             :             {
     884         251 :                 if (oStacExtension.ToString() == "proj" ||
     885         151 :                     oStacExtension.ToString().find(
     886             :                         "https://stac-extensions.github.io/projection/") == 0)
     887             :                 {
     888          50 :                     bProjExtensionFound = true;
     889          50 :                     break;
     890             :                 }
     891             :             }
     892          50 :             if (!bProjExtensionFound)
     893             :             {
     894           0 :                 CPLDebug(
     895             :                     "STACIT",
     896             :                     "Skipping Feature that lacks the 'proj' STAC extension");
     897           0 :                 continue;
     898             :             }
     899             : 
     900         100 :             auto jAssets = oFeature["assets"];
     901         100 :             if (!jAssets.IsValid() ||
     902          50 :                 jAssets.GetType() != CPLJSONObject::Type::Object)
     903             :             {
     904           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     905             :                          "Missing assets on a Feature");
     906           0 :                 continue;
     907             :             }
     908             : 
     909         100 :             auto oProperties = oFeature["properties"];
     910         100 :             if (!oProperties.IsValid() ||
     911          50 :                 oProperties.GetType() != CPLJSONObject::Type::Object)
     912             :             {
     913           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     914             :                          "Missing properties on a Feature");
     915           0 :                 continue;
     916             :             }
     917             : 
     918         100 :             const auto osCollection = oFeature["collection"].ToString();
     919          65 :             if (!osFilteredCollection.empty() &&
     920          15 :                 osFilteredCollection != osCollection)
     921           8 :                 continue;
     922             : 
     923          95 :             for (const auto &jAsset : jAssets.GetChildren())
     924             :             {
     925          53 :                 const auto osAssetName = jAsset.GetName();
     926          53 :                 if (!osFilteredAsset.empty() && osFilteredAsset != osAssetName)
     927           8 :                     continue;
     928             : 
     929          45 :                 ParseAsset(jAsset, oProperties, osCollection, osFilteredCRS,
     930             :                            oMapCollection);
     931             :             }
     932             :         }
     933          24 :         if (nMaxItems > 0 && nItemIter >= nMaxItems)
     934             :         {
     935           1 :             if (!bMaxItemsSpecified)
     936             :             {
     937           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     938             :                          "Maximum number of items (" CPL_FRMT_GIB
     939             :                          ") allowed to be retrieved has been hit",
     940             :                          nMaxItems);
     941             :             }
     942             :             else
     943             :             {
     944           1 :                 CPLDebug("STACIT",
     945             :                          "Maximum number of items (" CPL_FRMT_GIB
     946             :                          ") allowed to be retrieved has been hit",
     947             :                          nMaxItems);
     948             :             }
     949           1 :             break;
     950             :         }
     951             : 
     952             :         // Follow next link
     953             :         // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
     954          46 :         const auto oLinks = oRoot.GetArray("links");
     955          23 :         if (!oLinks.IsValid())
     956          20 :             break;
     957           6 :         std::string osNewFilename;
     958           6 :         for (const auto &oLink : oLinks)
     959             :         {
     960           6 :             const auto osType = oLink["type"].ToString();
     961           6 :             if (oLink["rel"].ToString() == "next" &&
     962           3 :                 (osType.empty() || osType == "application/geo+json"))
     963             :             {
     964           3 :                 osMethod = oLink.GetString("method", "GET");
     965           3 :                 osNewFilename = oLink["href"].ToString();
     966           3 :                 oHeaders = oLink["headers"];
     967           3 :                 oBody = oLink["body"];
     968           3 :                 bMerge = oLink.GetBool("merge", false);
     969           3 :                 if (osType == "application/geo+json")
     970             :                 {
     971           0 :                     break;
     972             :                 }
     973             :             }
     974             :         }
     975           6 :         if (!osNewFilename.empty() &&
     976           3 :             (osNewFilename != osCurFilename ||
     977           0 :              (oBody.IsValid() &&
     978           0 :               oBody.GetType() == CPLJSONObject::Type::Object)))
     979             :         {
     980           3 :             osCurFilename = osNewFilename;
     981             :         }
     982             :         else
     983           0 :             osCurFilename.clear();
     984           3 :     } while (!osCurFilename.empty());
     985             : 
     986          21 :     if (oMapCollection.empty())
     987             :     {
     988           2 :         CPLError(CE_Failure, CPLE_AppDefined, "No compatible asset found");
     989           2 :         return false;
     990             :     }
     991             : 
     992          37 :     if (oMapCollection.size() > 1 ||
     993          37 :         oMapCollection.begin()->second.assets.size() > 1 ||
     994          37 :         oMapCollection.begin()->second.assets.begin()->second.assets.size() > 1)
     995             :     {
     996             :         // If there's more than one asset type or more than one SRS, expose
     997             :         // subdatasets.
     998           1 :         SetSubdatasets(osFilename, oMapCollection);
     999           1 :         return true;
    1000             :     }
    1001             :     else
    1002             :     {
    1003          18 :         return SetupDataset(poOpenInfo, osFilename, oMapCollection);
    1004             :     }
    1005             : }
    1006             : 
    1007             : /************************************************************************/
    1008             : /*                            OpenStatic()                              */
    1009             : /************************************************************************/
    1010             : 
    1011          21 : GDALDataset *STACITDataset::OpenStatic(GDALOpenInfo *poOpenInfo)
    1012             : {
    1013          21 :     if (!Identify(poOpenInfo))
    1014           0 :         return nullptr;
    1015          42 :     auto poDS = std::make_unique<STACITDataset>();
    1016          21 :     if (!poDS->Open(poOpenInfo))
    1017           2 :         return nullptr;
    1018          19 :     return poDS.release();
    1019             : }
    1020             : 
    1021             : /************************************************************************/
    1022             : /*                       GDALRegister_STACIT()                          */
    1023             : /************************************************************************/
    1024             : 
    1025        2024 : void GDALRegister_STACIT()
    1026             : 
    1027             : {
    1028        2024 :     if (GDALGetDriverByName("STACIT") != nullptr)
    1029         283 :         return;
    1030             : 
    1031        1741 :     GDALDriver *poDriver = new GDALDriver();
    1032             : 
    1033        1741 :     poDriver->SetDescription("STACIT");
    1034        1741 :     poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    1035        1741 :     poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
    1036        1741 :                               "Spatio-Temporal Asset Catalog Items");
    1037        1741 :     poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/stacit.html");
    1038        1741 :     poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
    1039        1741 :     poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
    1040        1741 :     poDriver->SetMetadataItem(
    1041             :         GDAL_DMD_OPENOPTIONLIST,
    1042             :         "<OpenOptionList>"
    1043             :         "   <Option name='MAX_ITEMS' type='int' default='1000' "
    1044             :         "description='Maximum number of items fetched. 0=unlimited'/>"
    1045             :         "   <Option name='COLLECTION' type='string' "
    1046             :         "description='Name of collection to filter items'/>"
    1047             :         "   <Option name='ASSET' type='string' "
    1048             :         "description='Name of asset to filter items'/>"
    1049             :         "   <Option name='CRS' type='string' "
    1050             :         "description='Name of CRS to filter items'/>"
    1051             :         "   <Option name='RESOLUTION' type='string-select' default='AVERAGE' "
    1052             :         "description='Strategy to use to determine dataset resolution'>"
    1053             :         "       <Value>AVERAGE</Value>"
    1054             :         "       <Value>HIGHEST</Value>"
    1055             :         "       <Value>LOWEST</Value>"
    1056             :         "   </Option>"
    1057             :         "   <Option name='OVERLAP_STRATEGY' type='string-select' "
    1058             :         "default='REMOVE_IF_NO_NODATA' "
    1059             :         "description='Strategy to use when some sources are fully "
    1060             :         "covered by others'>"
    1061             :         "       <Value>REMOVE_IF_NO_NODATA</Value>"
    1062             :         "       <Value>USE_ALL</Value>"
    1063             :         "       <Value>USE_MOST_RECENT</Value>"
    1064             :         "   </Option>"
    1065        1741 :         "</OpenOptionList>");
    1066             : 
    1067        1741 :     poDriver->pfnOpen = STACITDataset::OpenStatic;
    1068        1741 :     poDriver->pfnIdentify = STACITDataset::Identify;
    1069             : 
    1070        1741 :     GetGDALDriverManager()->RegisterDriver(poDriver);
    1071             : }

Generated by: LCOV version 1.14