• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OSGeo / gdal / 15885686134

25 Jun 2025 07:44PM UTC coverage: 71.084%. Remained the same
15885686134

push

github

rouault
gdal_priv.h: fix C++11 compatibility

573814 of 807237 relevant lines covered (71.08%)

250621.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.16
/frmts/stacit/stacitdataset.cpp
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
class STACITDataset final : public VRTDataset
42✔
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
STACITDataset::STACITDataset() : VRTDataset(0, 0)
21✔
85
{
86
    poDriver = nullptr;  // cancel what the VRTDataset did
21✔
87
    SetWritable(false);
21✔
88
}
21✔
89

90
STACITDataset::~STACITDataset() = default;
91

92
/************************************************************************/
93
/*                             Identify()                               */
94
/************************************************************************/
95

96
int STACITDataset::Identify(GDALOpenInfo *poOpenInfo)
56,172✔
97
{
98
    if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
56,172✔
99
    {
100
        return true;
12✔
101
    }
102

103
    const bool bIsSingleDriver = poOpenInfo->IsSingleAllowedDriver("STACIT");
56,160✔
104
    if (bIsSingleDriver && (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
56,159✔
105
                            STARTS_WITH(poOpenInfo->pszFilename, "https://")))
4✔
106
    {
107
        return true;
1✔
108
    }
109

110
    if (poOpenInfo->nHeaderBytes == 0)
56,158✔
111
    {
112
        return false;
53,095✔
113
    }
114

115
    for (int i = 0; i < 2; i++)
9,125✔
116
    {
117
        // TryToIngest() may reallocate pabyHeader, so do not move this
118
        // before the loop.
119
        const char *pszHeader =
6,094✔
120
            reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
121
        while (*pszHeader != 0 &&
6,490✔
122
               std::isspace(static_cast<unsigned char>(*pszHeader)))
4,690✔
123
            ++pszHeader;
396✔
124
        if (bIsSingleDriver)
6,094✔
125
        {
126
            return pszHeader[0] == '{';
4✔
127
        }
128

129
        if (strstr(pszHeader, "\"stac_version\"") != nullptr)
6,090✔
130
        {
131
            int nTransformBBOXShapeCount = 0;
28✔
132
            for (const char *pszItem :
84✔
133
                 {"\"proj:transform\"", "\"proj:bbox\"", "\"proj:shape\""})
112✔
134
            {
135
                if (strstr(pszHeader, pszItem))
84✔
136
                    nTransformBBOXShapeCount++;
56✔
137
            }
138
            if (nTransformBBOXShapeCount >= 2)
28✔
139
            {
140
                return true;
28✔
141
            }
142
        }
143

144
        if (i == 0)
6,062✔
145
        {
146
            // Should be enough for a STACIT .json file
147
            poOpenInfo->TryToIngest(32768);
3,031✔
148
        }
149
    }
150

151
    return false;
3,031✔
152
}
153

154
/************************************************************************/
155
/*                        SanitizeCRSValue()                            */
156
/************************************************************************/
157

158
static std::string SanitizeCRSValue(const std::string &v)
27✔
159
{
160
    std::string ret;
27✔
161
    bool lastWasAlphaNum = true;
27✔
162
    for (char ch : v)
87✔
163
    {
164
        if (!isalnum(static_cast<unsigned char>(ch)))
60✔
165
        {
166
            if (lastWasAlphaNum)
6✔
167
                ret += '_';
6✔
168
            lastWasAlphaNum = false;
6✔
169
        }
170
        else
171
        {
172
            ret += ch;
54✔
173
            lastWasAlphaNum = true;
54✔
174
        }
175
    }
176
    if (!ret.empty() && ret.back() == '_')
27✔
177
        ret.pop_back();
×
178
    return ret;
27✔
179
}
180

181
/************************************************************************/
182
/*                            ParseAsset()                              */
183
/************************************************************************/
184

185
static void ParseAsset(const CPLJSONObject &jAsset,
45✔
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
    const auto osType = jAsset["type"].ToString();
90✔
193
    if (osType == "application/json" || osType == "application/xml" ||
84✔
194
        osType == "text/plain")
39✔
195
    {
196
        return;
6✔
197
    }
198

199
    // Skip assets whose role is obviously non georeferenced
200
    const auto oRoles = jAsset.GetArray("roles");
78✔
201
    if (oRoles.IsValid())
39✔
202
    {
203
        for (const auto &oRole : oRoles)
78✔
204
        {
205
            const auto osRole = oRole.ToString();
78✔
206
            if (osRole == "thumbnail" || osRole == "info" ||
78✔
207
                osRole == "metadata")
39✔
208
            {
209
                return;
×
210
            }
211
        }
212
    }
213

214
    const auto osAssetName = jAsset.GetName();
39✔
215

216
    std::string osHref = jAsset["href"].ToString();
78✔
217
    if (osHref.empty())
39✔
218
    {
219
        CPLError(CE_Warning, CPLE_AppDefined, "Missing href on asset %s",
×
220
                 osAssetName.c_str());
221
        return;
×
222
    }
223

224
    const auto GetAssetOrFeatureProperty =
225
        [&oProperties, &jAsset](const char *pszName)
301✔
226
    {
227
        auto obj = jAsset[pszName];
564✔
228
        if (obj.IsValid())
188✔
229
            return obj;
75✔
230
        return oProperties[pszName];
113✔
231
    };
39✔
232

233
    std::string osProjUserString;
39✔
234
    const auto oProjCode = GetAssetOrFeatureProperty("proj:code");
39✔
235
    if (oProjCode.IsValid() && oProjCode.GetType() != CPLJSONObject::Type::Null)
39✔
236
    {
237
        osProjUserString = oProjCode.ToString();
1✔
238
    }
239
    else
240
    {
241
        const auto oProjEPSG = GetAssetOrFeatureProperty("proj:epsg");
38✔
242
        if (oProjEPSG.IsValid() &&
76✔
243
            oProjEPSG.GetType() != CPLJSONObject::Type::Null)
38✔
244
        {
245
            osProjUserString = "EPSG:" + oProjEPSG.ToString();
38✔
246
        }
247
        else
248
        {
249
            const auto oProjWKT2 = GetAssetOrFeatureProperty("proj:wkt2");
×
250
            if (oProjWKT2.IsValid() &&
×
251
                oProjWKT2.GetType() == CPLJSONObject::Type::String)
×
252
            {
253
                osProjUserString = oProjWKT2.ToString();
×
254
            }
255
            else
256
            {
257
                const auto oProjPROJJSON =
258
                    GetAssetOrFeatureProperty("proj:projjson");
×
259
                if (oProjPROJJSON.IsValid() &&
×
260
                    oProjPROJJSON.GetType() == CPLJSONObject::Type::Object)
×
261
                {
262
                    osProjUserString = oProjPROJJSON.ToString();
×
263
                }
264
                else
265
                {
266
                    CPLDebug("STACIT",
×
267
                             "Skipping asset %s that lacks a valid CRS member",
268
                             osAssetName.c_str());
269
                    return;
×
270
                }
271
            }
272
        }
273
    }
274

275
    if (!osFilteredCRS.empty() &&
43✔
276
        osFilteredCRS != SanitizeCRSValue(osProjUserString))
43✔
277
    {
278
        return;
2✔
279
    }
280

281
    AssetItem item;
37✔
282
    item.osFilename = std::move(osHref);
37✔
283
    item.osDateTime = oProperties["datetime"].ToString();
37✔
284

285
    // Figure out item bounds and width/height
286
    auto oProjBBOX = GetAssetOrFeatureProperty("proj:bbox").ToArray();
37✔
287
    auto oProjShape = GetAssetOrFeatureProperty("proj:shape").ToArray();
37✔
288
    auto oProjTransform = GetAssetOrFeatureProperty("proj:transform").ToArray();
37✔
289
    const bool bIsBBOXValid = oProjBBOX.IsValid() && oProjBBOX.Size() == 4;
37✔
290
    const bool bIsShapeValid = oProjShape.IsValid() && oProjShape.Size() == 2;
37✔
291
    const bool bIsTransformValid =
292
        oProjTransform.IsValid() &&
74✔
293
        (oProjTransform.Size() == 6 || oProjTransform.Size() == 9);
37✔
294
    std::vector<double> bbox;
37✔
295
    if (bIsBBOXValid)
37✔
296
    {
297
        for (const auto &oItem : oProjBBOX)
130✔
298
            bbox.push_back(oItem.ToDouble());
104✔
299
        CPLAssert(bbox.size() == 4);
26✔
300
    }
301
    std::vector<int> shape;
37✔
302
    if (bIsShapeValid)
37✔
303
    {
304
        for (const auto &oItem : oProjShape)
33✔
305
            shape.push_back(oItem.ToInteger());
22✔
306
        CPLAssert(shape.size() == 2);
11✔
307
    }
308
    std::vector<double> transform;
37✔
309
    if (bIsTransformValid)
37✔
310
    {
311
        for (const auto &oItem : oProjTransform)
352✔
312
            transform.push_back(oItem.ToDouble());
315✔
313
        CPLAssert(transform.size() == 6 || transform.size() == 9);
37✔
314
#if defined(__GNUC__)
315
#pragma GCC diagnostic push
316
#pragma GCC diagnostic ignored "-Wnull-dereference"
317
#endif
318
        if (transform[0] <= 0 || transform[1] != 0 || transform[3] != 0 ||
74✔
319
            transform[4] >= 0 ||
142✔
320
            (transform.size() == 9 &&
37✔
321
             (transform[6] != 0 || transform[7] != 0 || transform[8] != 1)))
31✔
322
        {
323
            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
            return;
×
329
        }
330
#if defined(__GNUC__)
331
#pragma GCC diagnostic pop
332
#endif
333
    }
334

335
    if (bIsBBOXValid && bIsShapeValid)
37✔
336
    {
337
        item.nXSize = shape[1];
×
338
        item.nYSize = shape[0];
×
339
        item.dfXMin = bbox[0];
×
340
        item.dfYMin = bbox[1];
×
341
        item.dfXMax = bbox[2];
×
342
        item.dfYMax = bbox[3];
×
343
    }
344
    else if (bIsBBOXValid && bIsTransformValid)
37✔
345
    {
346
        item.dfXMin = bbox[0];
26✔
347
        item.dfYMin = bbox[1];
26✔
348
        item.dfXMax = bbox[2];
26✔
349
        item.dfYMax = bbox[3];
26✔
350
        if (item.dfXMin != transform[2] || item.dfYMax != transform[5])
26✔
351
        {
352
            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
            return;
×
357
        }
358
        double dfXSize = (item.dfXMax - item.dfXMin) / transform[0];
26✔
359
        double dfYSize = (item.dfYMax - item.dfYMin) / -transform[4];
26✔
360
        if (!(dfXSize < INT_MAX && dfYSize < INT_MAX))
26✔
361
            return;
×
362
        item.nXSize = static_cast<int>(dfXSize);
26✔
363
        item.nYSize = static_cast<int>(dfYSize);
26✔
364
    }
365
    else if (bIsShapeValid && bIsTransformValid)
11✔
366
    {
367
        item.nXSize = shape[1];
11✔
368
        item.nYSize = shape[0];
11✔
369
        item.dfXMin = transform[2];
11✔
370
        item.dfYMax = transform[5];
11✔
371
        item.dfXMax = item.dfXMin + item.nXSize * transform[0];
11✔
372
        item.dfYMin = item.dfYMax + item.nYSize * transform[4];
11✔
373
    }
374
    else
375
    {
376
        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
        return;
×
381
    }
382

383
    if (item.nXSize <= 0 || item.nYSize <= 0)
37✔
384
    {
385
        CPLError(CE_Warning, CPLE_AppDefined,
×
386
                 "Skipping asset %s because the size is invalid",
387
                 osAssetName.c_str());
388
        return;
×
389
    }
390

391
    // Create/fetch collection
392
    if (oMapCollection.find(osCollection) == oMapCollection.end())
37✔
393
    {
394
        Collection collection;
40✔
395
        collection.osName = osCollection;
20✔
396
        oMapCollection[osCollection] = std::move(collection);
20✔
397
    }
398
    auto &collection = oMapCollection[osCollection];
37✔
399

400
    // Create/fetch asset in collection
401
    if (collection.assets.find(osAssetName) == collection.assets.end())
37✔
402
    {
403
        Asset asset;
42✔
404
        asset.osName = osAssetName;
21✔
405
        asset.bands = jAsset.GetArray("bands");
21✔
406
        if (!asset.bands.IsValid())
21✔
407
            asset.bands = jAsset.GetArray("eo:bands");
20✔
408

409
        collection.assets[osAssetName] = std::move(asset);
21✔
410
    }
411
    auto &asset = collection.assets[osAssetName];
37✔
412

413
    // Create/fetch projection in asset
414
    if (asset.assets.find(osProjUserString) == asset.assets.end())
37✔
415
    {
416
        AssetSetByProjection assetByProj;
44✔
417
        assetByProj.osProjUserString = osProjUserString;
22✔
418
        asset.assets[osProjUserString] = std::move(assetByProj);
22✔
419
    }
420
    auto &assets = asset.assets[osProjUserString];
37✔
421

422
    // Add item
423
    assets.assets.emplace_back(std::move(item));
37✔
424
}
425

426
/************************************************************************/
427
/*                           SetupDataset()                             */
428
/************************************************************************/
429

430
bool STACITDataset::SetupDataset(
18✔
431
    GDALOpenInfo *poOpenInfo, const std::string &osSTACITFilename,
432
    std::map<std::string, Collection> &oMapCollection)
433
{
434
    auto &collection = oMapCollection.begin()->second;
18✔
435
    auto &asset = collection.assets.begin()->second;
18✔
436
    auto &assetByProj = asset.assets.begin()->second;
18✔
437
    auto &items = assetByProj.assets;
18✔
438

439
    // Compute global bounds and resolution
440
    double dfXMin = std::numeric_limits<double>::max();
18✔
441
    double dfYMin = std::numeric_limits<double>::max();
18✔
442
    double dfXMax = -std::numeric_limits<double>::max();
18✔
443
    double dfYMax = -std::numeric_limits<double>::max();
18✔
444
    double dfXRes = 0;
18✔
445
    double dfYRes = 0;
18✔
446
    const char *pszResolution = CSLFetchNameValueDef(
36✔
447
        poOpenInfo->papszOpenOptions, "RESOLUTION", "AVERAGE");
18✔
448
    for (const auto &assetItem : items)
51✔
449
    {
450
        dfXMin = std::min(dfXMin, assetItem.dfXMin);
33✔
451
        dfYMin = std::min(dfYMin, assetItem.dfYMin);
33✔
452
        dfXMax = std::max(dfXMax, assetItem.dfXMax);
33✔
453
        dfYMax = std::max(dfYMax, assetItem.dfYMax);
33✔
454
        const double dfThisXRes =
33✔
455
            (assetItem.dfXMax - assetItem.dfXMin) / assetItem.nXSize;
33✔
456
        const double dfThisYRes =
33✔
457
            (assetItem.dfYMax - assetItem.dfYMin) / assetItem.nYSize;
33✔
458
#ifdef DEBUG_VERBOSE
459
        CPLDebug("STACIT", "%s -> resx=%f resy=%f",
460
                 assetItem.osFilename.c_str(), dfThisXRes, dfThisYRes);
461
#endif
462
        if (dfXRes != 0 && EQUAL(pszResolution, "HIGHEST"))
33✔
463
        {
464
            dfXRes = std::min(dfXRes, dfThisXRes);
×
465
            dfYRes = std::min(dfYRes, dfThisYRes);
×
466
        }
467
        else if (dfXRes != 0 && EQUAL(pszResolution, "LOWEST"))
33✔
468
        {
469
            dfXRes = std::max(dfXRes, dfThisXRes);
×
470
            dfYRes = std::max(dfYRes, dfThisYRes);
×
471
        }
472
        else
473
        {
474
            dfXRes += dfThisXRes;
33✔
475
            dfYRes += dfThisYRes;
33✔
476
        }
477
    }
478
    if (EQUAL(pszResolution, "AVERAGE"))
18✔
479
    {
480
        dfXRes /= static_cast<int>(items.size());
18✔
481
        dfYRes /= static_cast<int>(items.size());
18✔
482
    }
483

484
    // Set raster size
485
    if (dfXRes == 0 || dfYRes == 0)
18✔
486
    {
487
        CPLError(CE_Failure, CPLE_AppDefined,
×
488
                 "Invalid computed dataset dimensions");
489
        return false;
×
490
    }
491
    double dfXSize = std::round((dfXMax - dfXMin) / dfXRes);
18✔
492
    double dfYSize = std::round((dfYMax - dfYMin) / dfYRes);
18✔
493
    if (dfXSize <= 0 || dfYSize <= 0 || dfXSize > INT_MAX || dfYSize > INT_MAX)
18✔
494
    {
495
        CPLError(CE_Failure, CPLE_AppDefined,
×
496
                 "Invalid computed dataset dimensions");
497
        return false;
×
498
    }
499
    nRasterXSize = static_cast<int>(dfXSize);
18✔
500
    nRasterYSize = static_cast<int>(dfYSize);
18✔
501

502
    // Set geotransform
503
    GDALGeoTransform gt{dfXMin, dfXRes, 0, dfYMax, 0, -dfYRes};
18✔
504
    SetGeoTransform(gt);
18✔
505

506
    // Set SRS
507
    OGRSpatialReference oSRS;
36✔
508
    if (oSRS.SetFromUserInput(
18✔
509
            assetByProj.osProjUserString.c_str(),
510
            OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
18✔
511
        OGRERR_NONE)
512
    {
513
        oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
18✔
514
        SetSpatialRef(&oSRS);
18✔
515
    }
516

517
    // Open of the items to find the number of bands, their data type
518
    // and nodata.
519
    const auto BuildVSICurlFilename =
520
        [&osSTACITFilename, &collection](const std::string &osFilename)
51✔
521
    {
522
        std::string osRet;
51✔
523
        if (STARTS_WITH(osFilename.c_str(), "http"))
51✔
524
        {
525
            if (STARTS_WITH(osSTACITFilename.c_str(),
×
526
                            "https://planetarycomputer.microsoft.com/api/"))
527
            {
528
                osRet = "/vsicurl?pc_url_signing=yes&pc_collection=";
×
529
                osRet += collection.osName;
×
530
                osRet += "&url=";
×
531
                char *pszEncoded =
532
                    CPLEscapeString(osFilename.c_str(), -1, CPLES_URL);
×
533
                CPLString osEncoded(pszEncoded);
×
534
                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
                osEncoded.replaceAll("%2F", '/');
×
539
                osRet += osEncoded;
×
540
            }
541
            else
542
            {
543
                osRet = "/vsicurl/";
×
544
                osRet += osFilename;
×
545
            }
546
        }
547
        else if (STARTS_WITH(osFilename.c_str(), "file://"))
51✔
548
        {
549
            osRet = osFilename.substr(strlen("file://"));
3✔
550
        }
551
        else if (STARTS_WITH(osFilename.c_str(), "s3://"))
48✔
552
        {
553
            osRet = "/vsis3/";
×
554
            osRet += osFilename.substr(strlen("s3://"));
×
555
        }
556

557
        else
558
        {
559
            osRet = osFilename;
48✔
560
        }
561
        return osRet;
51✔
562
    };
18✔
563

564
    const auto osFirstItemName(BuildVSICurlFilename(items.front().osFilename));
36✔
565
    auto poItemDS = std::unique_ptr<GDALDataset>(
566
        GDALDataset::Open(osFirstItemName.c_str()));
36✔
567
    if (!poItemDS)
18✔
568
    {
569
        CPLError(CE_Failure, CPLE_AppDefined,
×
570
                 "Cannot open %s to retrieve band characteristics",
571
                 osFirstItemName.c_str());
572
        return false;
×
573
    }
574

575
    // Sort by ascending datetime
576
    std::sort(items.begin(), items.end(),
18✔
577
              [](const AssetItem &a, const AssetItem &b)
26✔
578
              {
579
                  if (!a.osDateTime.empty() && !b.osDateTime.empty())
26✔
580
                      return a.osDateTime < b.osDateTime;
26✔
581
                  return &a < &b;
×
582
              });
583

584
    // Create VRT bands and add sources
585
    bool bAtLeastOneBandHasNoData = false;
18✔
586
    for (int i = 0; i < poItemDS->GetRasterCount(); i++)
36✔
587
    {
588
        auto poItemBand = poItemDS->GetRasterBand(i + 1);
18✔
589
        AddBand(poItemBand->GetRasterDataType(), nullptr);
18✔
590
        auto poVRTBand =
591
            cpl::down_cast<VRTSourcedRasterBand *>(GetRasterBand(i + 1));
18✔
592
        int bHasNoData = FALSE;
18✔
593
        const double dfNoData = poItemBand->GetNoDataValue(&bHasNoData);
18✔
594
        if (bHasNoData)
18✔
595
        {
596
            bAtLeastOneBandHasNoData = true;
4✔
597
            poVRTBand->SetNoDataValue(dfNoData);
4✔
598
        }
599

600
        const auto eInterp = poItemBand->GetColorInterpretation();
18✔
601
        if (eInterp != GCI_Undefined)
18✔
602
            poVRTBand->SetColorInterpretation(eInterp);
18✔
603

604
        // Set band properties
605
        if (asset.bands.IsValid() &&
36✔
606
            asset.bands.Size() == poItemDS->GetRasterCount())
18✔
607
        {
608
            const auto &band = asset.bands[i];
36✔
609
            const auto osBandName = band["name"].ToString();
54✔
610
            if (!osBandName.empty())
18✔
611
                poVRTBand->SetDescription(osBandName.c_str());
18✔
612

613
            auto osCommonName = band["eo:common_name"].ToString();
54✔
614
            if (osCommonName.empty())
18✔
615
                osCommonName = band["common_name"].ToString();
17✔
616
            if (!osCommonName.empty())
18✔
617
            {
618
                const auto eInterpFromCommonName =
619
                    GDALGetColorInterpFromSTACCommonName(osCommonName.c_str());
14✔
620
                if (eInterpFromCommonName != GCI_Undefined)
14✔
621
                    poVRTBand->SetColorInterpretation(eInterpFromCommonName);
14✔
622
            }
623

624
            for (const auto &bandChild : band.GetChildren())
78✔
625
            {
626
                const auto osChildName = bandChild.GetName();
120✔
627
                if (osChildName != "name" && osChildName != "common_name" &&
89✔
628
                    osChildName != "eo:common_name")
29✔
629
                {
630
                    poVRTBand->SetMetadataItem(osChildName.c_str(),
28✔
631
                                               bandChild.ToString().c_str());
56✔
632
                }
633
            }
634
        }
635

636
        // Add items as VRT sources
637
        for (const auto &assetItem : items)
51✔
638
        {
639
            const auto osItemName(BuildVSICurlFilename(assetItem.osFilename));
66✔
640
            const double dfDstXOff = (assetItem.dfXMin - dfXMin) / dfXRes;
33✔
641
            const double dfDstXSize =
33✔
642
                (assetItem.dfXMax - assetItem.dfXMin) / dfXRes;
33✔
643
            const double dfDstYOff = (dfYMax - assetItem.dfYMax) / dfYRes;
33✔
644
            const double dfDstYSize =
33✔
645
                (assetItem.dfYMax - assetItem.dfYMin) / dfYRes;
33✔
646
            if (!bHasNoData)
33✔
647
            {
648
                poVRTBand->AddSimpleSource(osItemName.c_str(), i + 1, 0, 0,
25✔
649
                                           assetItem.nXSize, assetItem.nYSize,
25✔
650
                                           dfDstXOff, dfDstYOff, dfDstXSize,
651
                                           dfDstYSize);
652
            }
653
            else
654
            {
655
                poVRTBand->AddComplexSource(osItemName.c_str(), i + 1, 0, 0,
8✔
656
                                            assetItem.nXSize, assetItem.nYSize,
8✔
657
                                            dfDstXOff, dfDstYOff, dfDstXSize,
658
                                            dfDstYSize,
659
                                            0.0,  // offset
660
                                            1.0,  // scale
661
                                            dfNoData);
662
            }
663
        }
664

665
        const char *pszOverlapStrategy =
666
            CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
18✔
667
                                 "OVERLAP_STRATEGY", "REMOVE_IF_NO_NODATA");
668
        if ((EQUAL(pszOverlapStrategy, "REMOVE_IF_NO_NODATA") &&
18✔
669
             !bAtLeastOneBandHasNoData) ||
14✔
670
            EQUAL(pszOverlapStrategy, "USE_MOST_RECENT"))
6✔
671
        {
672
            const char *const apszOptions[] = {
14✔
673
                "EMIT_ERROR_IF_GEOS_NOT_AVAILABLE=NO", nullptr};
674
            poVRTBand->RemoveCoveredSources(apszOptions);
14✔
675
        }
676
    }
677
    return true;
18✔
678
}
679

680
/************************************************************************/
681
/*                         SetSubdatasets()                             */
682
/************************************************************************/
683

684
void STACITDataset::SetSubdatasets(
1✔
685
    const std::string &osFilename,
686
    const std::map<std::string, Collection> &oMapCollection)
687
{
688
    CPLStringList aosSubdatasets;
2✔
689
    int nCount = 1;
1✔
690
    for (const auto &collectionKV : oMapCollection)
3✔
691
    {
692
        for (const auto &assetKV : collectionKV.second.assets)
5✔
693
        {
694
            std::string osCollectionAssetArg;
6✔
695
            if (oMapCollection.size() > 1)
3✔
696
                osCollectionAssetArg +=
697
                    "collection=" + collectionKV.first + ",";
3✔
698
            osCollectionAssetArg += "asset=" + assetKV.first;
3✔
699

700
            std::string osCollectionAssetText;
6✔
701
            if (oMapCollection.size() > 1)
3✔
702
                osCollectionAssetText +=
703
                    "Collection " + collectionKV.first + ", ";
3✔
704
            osCollectionAssetText += "Asset " + assetKV.first;
3✔
705

706
            if (assetKV.second.assets.size() == 1)
3✔
707
            {
708
                aosSubdatasets.AddString(CPLSPrintf(
709
                    "SUBDATASET_%d_NAME=STACIT:\"%s\":%s", nCount,
710
                    osFilename.c_str(), osCollectionAssetArg.c_str()));
2✔
711
                aosSubdatasets.AddString(CPLSPrintf(
712
                    "SUBDATASET_%d_DESC=%s of %s", nCount,
713
                    osCollectionAssetText.c_str(), osFilename.c_str()));
2✔
714
                nCount++;
2✔
715
            }
716
            else
717
            {
718
                for (const auto &assetsByProjKV : assetKV.second.assets)
3✔
719
                {
720
                    aosSubdatasets.AddString(CPLSPrintf(
721
                        "SUBDATASET_%d_NAME=STACIT:\"%s\":%s,crs=%s", nCount,
722
                        osFilename.c_str(), osCollectionAssetArg.c_str(),
723
                        SanitizeCRSValue(assetsByProjKV.first).c_str()));
2✔
724
                    aosSubdatasets.AddString(CPLSPrintf(
725
                        "SUBDATASET_%d_DESC=%s of %s in CRS %s", nCount,
726
                        osCollectionAssetText.c_str(), osFilename.c_str(),
727
                        assetsByProjKV.first.c_str()));
2✔
728
                    nCount++;
2✔
729
                }
730
            }
731
        }
732
    }
733
    GDALDataset::SetMetadata(aosSubdatasets.List(), "SUBDATASETS");
1✔
734
}
1✔
735

736
/************************************************************************/
737
/*                               Open()                                 */
738
/************************************************************************/
739

740
bool STACITDataset::Open(GDALOpenInfo *poOpenInfo)
21✔
741
{
742
    CPLString osFilename(poOpenInfo->pszFilename);
42✔
743
    std::string osFilteredCollection =
744
        CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "COLLECTION", "");
42✔
745
    std::string osFilteredAsset =
746
        CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ASSET", "");
42✔
747
    std::string osFilteredCRS = SanitizeCRSValue(
748
        CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", ""));
63✔
749
    if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
21✔
750
    {
751
        const CPLStringList aosTokens(CSLTokenizeString2(
752
            poOpenInfo->pszFilename, ":", CSLT_HONOURSTRINGS));
6✔
753
        if (aosTokens.size() != 2 && aosTokens.size() != 3)
6✔
754
            return false;
×
755
        osFilename = aosTokens[1];
6✔
756
        if (aosTokens.size() >= 3)
6✔
757
        {
758
            const CPLStringList aosFilters(
759
                CSLTokenizeString2(aosTokens[2], ",", 0));
12✔
760
            osFilteredCollection = aosFilters.FetchNameValueDef(
761
                "collection", osFilteredCollection.c_str());
6✔
762
            osFilteredAsset =
763
                aosFilters.FetchNameValueDef("asset", osFilteredAsset.c_str());
6✔
764
            osFilteredCRS =
765
                aosFilters.FetchNameValueDef("crs", osFilteredCRS.c_str());
6✔
766
        }
767
    }
768

769
    std::map<std::string, Collection> oMapCollection;
42✔
770
    GIntBig nItemIter = 0;
21✔
771
    GIntBig nMaxItems = CPLAtoGIntBig(CSLFetchNameValueDef(
21✔
772
        poOpenInfo->papszOpenOptions, "MAX_ITEMS", "1000"));
21✔
773

774
    const bool bMaxItemsSpecified =
775
        CSLFetchNameValue(poOpenInfo->papszOpenOptions, "MAX_ITEMS") != nullptr;
21✔
776
    if (!bMaxItemsSpecified)
21✔
777
    {
778
        // If the URL includes a limit parameter, and it's larger than our
779
        // default MAX_ITEMS value, then increase the later to the former.
780
        const auto nPos = osFilename.ifind("&limit=");
20✔
781
        if (nPos != std::string::npos)
20✔
782
        {
783
            const auto nLimit = CPLAtoGIntBig(
×
784
                osFilename.substr(nPos + strlen("&limit=")).c_str());
×
785
            nMaxItems = std::max(nMaxItems, nLimit);
×
786
        }
787
    }
788

789
    auto osCurFilename = osFilename;
42✔
790
    std::string osMethod = "GET";
42✔
791
    CPLJSONObject oHeaders;
42✔
792
    CPLJSONObject oBody;
42✔
793
    bool bMerge = false;
21✔
794
    int nLoops = 0;
21✔
795
    do
3✔
796
    {
797
        ++nLoops;
24✔
798
        if (nMaxItems > 0 && nLoops > nMaxItems)
24✔
799
        {
800
            break;
21✔
801
        }
802

803
        CPLJSONDocument oDoc;
24✔
804

805
        if (STARTS_WITH(osCurFilename, "http://") ||
47✔
806
            STARTS_WITH(osCurFilename, "https://"))
23✔
807
        {
808
            // Cf // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
809
            CPLStringList aosOptions;
1✔
810
            if (oBody.IsValid() &&
2✔
811
                oBody.GetType() == CPLJSONObject::Type::Object)
1✔
812
            {
813
                if (bMerge)
1✔
814
                    CPLDebug("STACIT",
×
815
                             "Ignoring 'merge' attribute from next link");
816
                const std::string osPostContent =
817
                    oBody.Format(CPLJSONObject::PrettyFormat::Pretty);
2✔
818
                aosOptions.SetNameValue("POSTFIELDS", osPostContent.c_str());
1✔
819
            }
820
            aosOptions.SetNameValue("CUSTOMREQUEST", osMethod.c_str());
1✔
821
            CPLString osHeaders;
1✔
822
            if (!oHeaders.IsValid() ||
4✔
823
                oHeaders.GetType() != CPLJSONObject::Type::Object ||
2✔
824
                oHeaders["Content-Type"].ToString().empty())
2✔
825
            {
826
                osHeaders = "Content-Type: application/json";
1✔
827
            }
828
            if (oHeaders.IsValid() &&
2✔
829
                oHeaders.GetType() == CPLJSONObject::Type::Object)
1✔
830
            {
831
                for (const auto &obj : oHeaders.GetChildren())
2✔
832
                {
833
                    osHeaders += "\r\n";
1✔
834
                    osHeaders += obj.GetName();
1✔
835
                    osHeaders += ": ";
1✔
836
                    osHeaders += obj.ToString();
1✔
837
                }
838
            }
839
            aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
1✔
840
            CPLHTTPResult *psResult =
841
                CPLHTTPFetch(osCurFilename.c_str(), aosOptions.List());
1✔
842
            if (!psResult)
1✔
843
                return false;
×
844
            if (!psResult->pabyData)
1✔
845
            {
846
                CPLHTTPDestroyResult(psResult);
×
847
                return false;
×
848
            }
849
            const bool bOK = oDoc.LoadMemory(
2✔
850
                reinterpret_cast<const char *>(psResult->pabyData));
1✔
851
            // CPLDebug("STACIT", "Response: %s", reinterpret_cast<const char*>(psResult->pabyData));
852
            CPLHTTPDestroyResult(psResult);
1✔
853
            if (!bOK)
1✔
854
                return false;
×
855
        }
856
        else
857
        {
858
            if (!oDoc.Load(osCurFilename))
23✔
859
                return false;
×
860
        }
861
        const auto oRoot = oDoc.GetRoot();
24✔
862
        auto oFeatures = oRoot.GetArray("features");
48✔
863
        if (!oFeatures.IsValid())
24✔
864
        {
865
            if (oRoot.GetString("type") == "Feature")
2✔
866
            {
867
                oFeatures = CPLJSONArray();
2✔
868
                oFeatures.Add(oRoot);
2✔
869
            }
870
            else
871
            {
872
                CPLError(CE_Failure, CPLE_AppDefined, "Missing features");
×
873
                return false;
×
874
            }
875
        }
876
        for (const auto &oFeature : oFeatures)
74✔
877
        {
878
            nItemIter++;
50✔
879
            if (nMaxItems > 0 && nItemIter > nMaxItems)
50✔
880
            {
881
                break;
×
882
            }
883

884
            auto oStacExtensions = oFeature.GetArray("stac_extensions");
100✔
885
            if (!oStacExtensions.IsValid())
50✔
886
            {
887
                CPLDebug("STACIT",
×
888
                         "Skipping Feature that lacks stac_extensions");
889
                continue;
×
890
            }
891
            bool bProjExtensionFound = false;
50✔
892
            for (const auto &oStacExtension : oStacExtensions)
150✔
893
            {
894
                if (oStacExtension.ToString() == "proj" ||
251✔
895
                    oStacExtension.ToString().find(
151✔
896
                        "https://stac-extensions.github.io/projection/") == 0)
897
                {
898
                    bProjExtensionFound = true;
50✔
899
                    break;
50✔
900
                }
901
            }
902
            if (!bProjExtensionFound)
50✔
903
            {
904
                CPLDebug(
×
905
                    "STACIT",
906
                    "Skipping Feature that lacks the 'proj' STAC extension");
907
                continue;
×
908
            }
909

910
            auto jAssets = oFeature["assets"];
100✔
911
            if (!jAssets.IsValid() ||
100✔
912
                jAssets.GetType() != CPLJSONObject::Type::Object)
50✔
913
            {
914
                CPLError(CE_Warning, CPLE_AppDefined,
×
915
                         "Missing assets on a Feature");
916
                continue;
×
917
            }
918

919
            auto oProperties = oFeature["properties"];
100✔
920
            if (!oProperties.IsValid() ||
100✔
921
                oProperties.GetType() != CPLJSONObject::Type::Object)
50✔
922
            {
923
                CPLError(CE_Warning, CPLE_AppDefined,
×
924
                         "Missing properties on a Feature");
925
                continue;
×
926
            }
927

928
            const auto osCollection = oFeature["collection"].ToString();
100✔
929
            if (!osFilteredCollection.empty() &&
65✔
930
                osFilteredCollection != osCollection)
15✔
931
                continue;
8✔
932

933
            for (const auto &jAsset : jAssets.GetChildren())
95✔
934
            {
935
                const auto osAssetName = jAsset.GetName();
53✔
936
                if (!osFilteredAsset.empty() && osFilteredAsset != osAssetName)
53✔
937
                    continue;
8✔
938

939
                ParseAsset(jAsset, oProperties, osCollection, osFilteredCRS,
45✔
940
                           oMapCollection);
941
            }
942
        }
943
        if (nMaxItems > 0 && nItemIter >= nMaxItems)
24✔
944
        {
945
            if (!bMaxItemsSpecified)
1✔
946
            {
947
                CPLError(CE_Warning, CPLE_AppDefined,
×
948
                         "Maximum number of items (" CPL_FRMT_GIB
949
                         ") allowed to be retrieved has been hit",
950
                         nMaxItems);
951
            }
952
            else
953
            {
954
                CPLDebug("STACIT",
1✔
955
                         "Maximum number of items (" CPL_FRMT_GIB
956
                         ") allowed to be retrieved has been hit",
957
                         nMaxItems);
958
            }
959
            break;
1✔
960
        }
961

962
        // Follow next link
963
        // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
964
        const auto oLinks = oRoot.GetArray("links");
46✔
965
        if (!oLinks.IsValid())
23✔
966
            break;
20✔
967
        std::string osNewFilename;
6✔
968
        for (const auto &oLink : oLinks)
6✔
969
        {
970
            const auto osType = oLink["type"].ToString();
6✔
971
            if (oLink["rel"].ToString() == "next" &&
6✔
972
                (osType.empty() || osType == "application/geo+json"))
3✔
973
            {
974
                osMethod = oLink.GetString("method", "GET");
3✔
975
                osNewFilename = oLink["href"].ToString();
3✔
976
                oHeaders = oLink["headers"];
3✔
977
                oBody = oLink["body"];
3✔
978
                bMerge = oLink.GetBool("merge", false);
3✔
979
                if (osType == "application/geo+json")
3✔
980
                {
981
                    break;
×
982
                }
983
            }
984
        }
985
        if (!osNewFilename.empty() &&
6✔
986
            (osNewFilename != osCurFilename ||
3✔
987
             (oBody.IsValid() &&
×
988
              oBody.GetType() == CPLJSONObject::Type::Object)))
×
989
        {
990
            osCurFilename = osNewFilename;
3✔
991
        }
992
        else
993
            osCurFilename.clear();
×
994
    } while (!osCurFilename.empty());
3✔
995

996
    if (oMapCollection.empty())
21✔
997
    {
998
        CPLError(CE_Failure, CPLE_AppDefined, "No compatible asset found");
2✔
999
        return false;
2✔
1000
    }
1001

1002
    if (oMapCollection.size() > 1 ||
37✔
1003
        oMapCollection.begin()->second.assets.size() > 1 ||
37✔
1004
        oMapCollection.begin()->second.assets.begin()->second.assets.size() > 1)
37✔
1005
    {
1006
        // If there's more than one asset type or more than one SRS, expose
1007
        // subdatasets.
1008
        SetSubdatasets(osFilename, oMapCollection);
1✔
1009
        return true;
1✔
1010
    }
1011
    else
1012
    {
1013
        return SetupDataset(poOpenInfo, osFilename, oMapCollection);
18✔
1014
    }
1015
}
1016

1017
/************************************************************************/
1018
/*                            OpenStatic()                              */
1019
/************************************************************************/
1020

1021
GDALDataset *STACITDataset::OpenStatic(GDALOpenInfo *poOpenInfo)
21✔
1022
{
1023
    if (!Identify(poOpenInfo))
21✔
1024
        return nullptr;
×
1025
    auto poDS = std::make_unique<STACITDataset>();
42✔
1026
    if (!poDS->Open(poOpenInfo))
21✔
1027
        return nullptr;
2✔
1028
    return poDS.release();
19✔
1029
}
1030

1031
/************************************************************************/
1032
/*                       GDALRegister_STACIT()                          */
1033
/************************************************************************/
1034

1035
void GDALRegister_STACIT()
1,911✔
1036

1037
{
1038
    if (GDALGetDriverByName("STACIT") != nullptr)
1,911✔
1039
        return;
282✔
1040

1041
    GDALDriver *poDriver = new GDALDriver();
1,629✔
1042

1043
    poDriver->SetDescription("STACIT");
1,629✔
1044
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
1,629✔
1045
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
1,629✔
1046
                              "Spatio-Temporal Asset Catalog Items");
1,629✔
1047
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/stacit.html");
1,629✔
1048
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
1,629✔
1049
    poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
1,629✔
1050
    poDriver->SetMetadataItem(
1,629✔
1051
        GDAL_DMD_OPENOPTIONLIST,
1052
        "<OpenOptionList>"
1053
        "   <Option name='MAX_ITEMS' type='int' default='1000' "
1054
        "description='Maximum number of items fetched. 0=unlimited'/>"
1055
        "   <Option name='COLLECTION' type='string' "
1056
        "description='Name of collection to filter items'/>"
1057
        "   <Option name='ASSET' type='string' "
1058
        "description='Name of asset to filter items'/>"
1059
        "   <Option name='CRS' type='string' "
1060
        "description='Name of CRS to filter items'/>"
1061
        "   <Option name='RESOLUTION' type='string-select' default='AVERAGE' "
1062
        "description='Strategy to use to determine dataset resolution'>"
1063
        "       <Value>AVERAGE</Value>"
1064
        "       <Value>HIGHEST</Value>"
1065
        "       <Value>LOWEST</Value>"
1066
        "   </Option>"
1067
        "   <Option name='OVERLAP_STRATEGY' type='string-select' "
1068
        "default='REMOVE_IF_NO_NODATA' "
1069
        "description='Strategy to use when some sources are fully "
1070
        "covered by others'>"
1071
        "       <Value>REMOVE_IF_NO_NODATA</Value>"
1072
        "       <Value>USE_ALL</Value>"
1073
        "       <Value>USE_MOST_RECENT</Value>"
1074
        "   </Option>"
1075
        "</OpenOptionList>");
1,629✔
1076

1077
    poDriver->pfnOpen = STACITDataset::OpenStatic;
1,629✔
1078
    poDriver->pfnIdentify = STACITDataset::Identify;
1,629✔
1079

1080
    GetGDALDriverManager()->RegisterDriver(poDriver);
1,629✔
1081
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc