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