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