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