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

Generated by: LCOV version 1.14