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 57193 : int STACITDataset::Identify(GDALOpenInfo *poOpenInfo)
97 : {
98 57193 : if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
99 : {
100 12 : return true;
101 : }
102 :
103 57181 : const bool bIsSingleDriver = poOpenInfo->IsSingleAllowedDriver("STACIT");
104 57178 : if (bIsSingleDriver && (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
105 4 : STARTS_WITH(poOpenInfo->pszFilename, "https://")))
106 : {
107 1 : return true;
108 : }
109 :
110 57177 : if (poOpenInfo->nHeaderBytes == 0)
111 : {
112 54042 : return false;
113 : }
114 :
115 9335 : for (int i = 0; i < 2; i++)
116 : {
117 : // TryToIngest() may reallocate pabyHeader, so do not move this
118 : // before the loop.
119 6234 : const char *pszHeader =
120 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
121 6630 : while (*pszHeader != 0 &&
122 4794 : std::isspace(static_cast<unsigned char>(*pszHeader)))
123 396 : ++pszHeader;
124 6234 : if (bIsSingleDriver)
125 : {
126 4 : return pszHeader[0] == '{';
127 : }
128 :
129 6230 : 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 6202 : if (i == 0)
145 : {
146 : // Should be enough for a STACIT .json file
147 3101 : poOpenInfo->TryToIngest(32768);
148 : }
149 : }
150 :
151 3101 : 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 : else
548 : {
549 51 : osRet = VSIURIToVSIPath(osFilename);
550 : }
551 51 : return osRet;
552 18 : };
553 :
554 36 : const auto osFirstItemName(BuildVSICurlFilename(items.front().osFilename));
555 : auto poItemDS = std::unique_ptr<GDALDataset>(
556 36 : GDALDataset::Open(osFirstItemName.c_str()));
557 18 : if (!poItemDS)
558 : {
559 0 : CPLError(CE_Failure, CPLE_AppDefined,
560 : "Cannot open %s to retrieve band characteristics",
561 : osFirstItemName.c_str());
562 0 : return false;
563 : }
564 :
565 : // Sort by ascending datetime
566 18 : std::sort(items.begin(), items.end(),
567 26 : [](const AssetItem &a, const AssetItem &b)
568 : {
569 26 : if (!a.osDateTime.empty() && !b.osDateTime.empty())
570 26 : return a.osDateTime < b.osDateTime;
571 0 : return &a < &b;
572 : });
573 :
574 : // Create VRT bands and add sources
575 18 : bool bAtLeastOneBandHasNoData = false;
576 36 : for (int i = 0; i < poItemDS->GetRasterCount(); i++)
577 : {
578 18 : auto poItemBand = poItemDS->GetRasterBand(i + 1);
579 18 : AddBand(poItemBand->GetRasterDataType(), nullptr);
580 : auto poVRTBand =
581 18 : cpl::down_cast<VRTSourcedRasterBand *>(GetRasterBand(i + 1));
582 18 : int bHasNoData = FALSE;
583 18 : const double dfNoData = poItemBand->GetNoDataValue(&bHasNoData);
584 18 : if (bHasNoData)
585 : {
586 4 : bAtLeastOneBandHasNoData = true;
587 4 : poVRTBand->SetNoDataValue(dfNoData);
588 : }
589 :
590 18 : const auto eInterp = poItemBand->GetColorInterpretation();
591 18 : if (eInterp != GCI_Undefined)
592 18 : poVRTBand->SetColorInterpretation(eInterp);
593 :
594 : // Set band properties
595 36 : if (asset.bands.IsValid() &&
596 18 : asset.bands.Size() == poItemDS->GetRasterCount())
597 : {
598 36 : const auto &band = asset.bands[i];
599 54 : const auto osBandName = band["name"].ToString();
600 18 : if (!osBandName.empty())
601 18 : poVRTBand->SetDescription(osBandName.c_str());
602 :
603 54 : auto osCommonName = band["eo:common_name"].ToString();
604 18 : if (osCommonName.empty())
605 17 : osCommonName = band["common_name"].ToString();
606 18 : if (!osCommonName.empty())
607 : {
608 : const auto eInterpFromCommonName =
609 14 : GDALGetColorInterpFromSTACCommonName(osCommonName.c_str());
610 14 : if (eInterpFromCommonName != GCI_Undefined)
611 14 : poVRTBand->SetColorInterpretation(eInterpFromCommonName);
612 : }
613 :
614 78 : for (const auto &bandChild : band.GetChildren())
615 : {
616 120 : const auto osChildName = bandChild.GetName();
617 89 : if (osChildName != "name" && osChildName != "common_name" &&
618 29 : osChildName != "eo:common_name")
619 : {
620 28 : poVRTBand->SetMetadataItem(osChildName.c_str(),
621 56 : bandChild.ToString().c_str());
622 : }
623 : }
624 : }
625 :
626 : // Add items as VRT sources
627 51 : for (const auto &assetItem : items)
628 : {
629 66 : const auto osItemName(BuildVSICurlFilename(assetItem.osFilename));
630 33 : const double dfDstXOff = (assetItem.dfXMin - dfXMin) / dfXRes;
631 33 : const double dfDstXSize =
632 33 : (assetItem.dfXMax - assetItem.dfXMin) / dfXRes;
633 33 : const double dfDstYOff = (dfYMax - assetItem.dfYMax) / dfYRes;
634 33 : const double dfDstYSize =
635 33 : (assetItem.dfYMax - assetItem.dfYMin) / dfYRes;
636 33 : if (!bHasNoData)
637 : {
638 25 : poVRTBand->AddSimpleSource(osItemName.c_str(), i + 1, 0, 0,
639 25 : assetItem.nXSize, assetItem.nYSize,
640 : dfDstXOff, dfDstYOff, dfDstXSize,
641 : dfDstYSize);
642 : }
643 : else
644 : {
645 8 : poVRTBand->AddComplexSource(osItemName.c_str(), i + 1, 0, 0,
646 8 : assetItem.nXSize, assetItem.nYSize,
647 : dfDstXOff, dfDstYOff, dfDstXSize,
648 : dfDstYSize,
649 : 0.0, // offset
650 : 1.0, // scale
651 : dfNoData);
652 : }
653 : }
654 :
655 : const char *pszOverlapStrategy =
656 18 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
657 : "OVERLAP_STRATEGY", "REMOVE_IF_NO_NODATA");
658 18 : if ((EQUAL(pszOverlapStrategy, "REMOVE_IF_NO_NODATA") &&
659 14 : !bAtLeastOneBandHasNoData) ||
660 6 : EQUAL(pszOverlapStrategy, "USE_MOST_RECENT"))
661 : {
662 14 : const char *const apszOptions[] = {
663 : "EMIT_ERROR_IF_GEOS_NOT_AVAILABLE=NO", nullptr};
664 14 : poVRTBand->RemoveCoveredSources(apszOptions);
665 : }
666 : }
667 18 : return true;
668 : }
669 :
670 : /************************************************************************/
671 : /* SetSubdatasets() */
672 : /************************************************************************/
673 :
674 1 : void STACITDataset::SetSubdatasets(
675 : const std::string &osFilename,
676 : const std::map<std::string, Collection> &oMapCollection)
677 : {
678 2 : CPLStringList aosSubdatasets;
679 1 : int nCount = 1;
680 3 : for (const auto &collectionKV : oMapCollection)
681 : {
682 5 : for (const auto &assetKV : collectionKV.second.assets)
683 : {
684 6 : std::string osCollectionAssetArg;
685 3 : if (oMapCollection.size() > 1)
686 : osCollectionAssetArg +=
687 3 : "collection=" + collectionKV.first + ",";
688 3 : osCollectionAssetArg += "asset=" + assetKV.first;
689 :
690 6 : std::string osCollectionAssetText;
691 3 : if (oMapCollection.size() > 1)
692 : osCollectionAssetText +=
693 3 : "Collection " + collectionKV.first + ", ";
694 3 : osCollectionAssetText += "Asset " + assetKV.first;
695 :
696 3 : if (assetKV.second.assets.size() == 1)
697 : {
698 : aosSubdatasets.AddString(CPLSPrintf(
699 : "SUBDATASET_%d_NAME=STACIT:\"%s\":%s", nCount,
700 2 : osFilename.c_str(), osCollectionAssetArg.c_str()));
701 : aosSubdatasets.AddString(CPLSPrintf(
702 : "SUBDATASET_%d_DESC=%s of %s", nCount,
703 2 : osCollectionAssetText.c_str(), osFilename.c_str()));
704 2 : nCount++;
705 : }
706 : else
707 : {
708 3 : for (const auto &assetsByProjKV : assetKV.second.assets)
709 : {
710 : aosSubdatasets.AddString(CPLSPrintf(
711 : "SUBDATASET_%d_NAME=STACIT:\"%s\":%s,crs=%s", nCount,
712 : osFilename.c_str(), osCollectionAssetArg.c_str(),
713 2 : SanitizeCRSValue(assetsByProjKV.first).c_str()));
714 : aosSubdatasets.AddString(CPLSPrintf(
715 : "SUBDATASET_%d_DESC=%s of %s in CRS %s", nCount,
716 : osCollectionAssetText.c_str(), osFilename.c_str(),
717 2 : assetsByProjKV.first.c_str()));
718 2 : nCount++;
719 : }
720 : }
721 : }
722 : }
723 1 : GDALDataset::SetMetadata(aosSubdatasets.List(), "SUBDATASETS");
724 1 : }
725 :
726 : /************************************************************************/
727 : /* Open() */
728 : /************************************************************************/
729 :
730 21 : bool STACITDataset::Open(GDALOpenInfo *poOpenInfo)
731 : {
732 42 : CPLString osFilename(poOpenInfo->pszFilename);
733 : std::string osFilteredCollection =
734 42 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "COLLECTION", "");
735 : std::string osFilteredAsset =
736 42 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ASSET", "");
737 : std::string osFilteredCRS = SanitizeCRSValue(
738 63 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", ""));
739 21 : if (STARTS_WITH(poOpenInfo->pszFilename, "STACIT:"))
740 : {
741 : const CPLStringList aosTokens(CSLTokenizeString2(
742 6 : poOpenInfo->pszFilename, ":", CSLT_HONOURSTRINGS));
743 6 : if (aosTokens.size() != 2 && aosTokens.size() != 3)
744 0 : return false;
745 6 : osFilename = aosTokens[1];
746 6 : if (aosTokens.size() >= 3)
747 : {
748 : const CPLStringList aosFilters(
749 12 : CSLTokenizeString2(aosTokens[2], ",", 0));
750 : osFilteredCollection = aosFilters.FetchNameValueDef(
751 6 : "collection", osFilteredCollection.c_str());
752 : osFilteredAsset =
753 6 : aosFilters.FetchNameValueDef("asset", osFilteredAsset.c_str());
754 : osFilteredCRS =
755 6 : aosFilters.FetchNameValueDef("crs", osFilteredCRS.c_str());
756 : }
757 : }
758 :
759 42 : std::map<std::string, Collection> oMapCollection;
760 21 : GIntBig nItemIter = 0;
761 21 : GIntBig nMaxItems = CPLAtoGIntBig(CSLFetchNameValueDef(
762 21 : poOpenInfo->papszOpenOptions, "MAX_ITEMS", "1000"));
763 :
764 : const bool bMaxItemsSpecified =
765 21 : CSLFetchNameValue(poOpenInfo->papszOpenOptions, "MAX_ITEMS") != nullptr;
766 21 : if (!bMaxItemsSpecified)
767 : {
768 : // If the URL includes a limit parameter, and it's larger than our
769 : // default MAX_ITEMS value, then increase the later to the former.
770 20 : const auto nPos = osFilename.ifind("&limit=");
771 20 : if (nPos != std::string::npos)
772 : {
773 0 : const auto nLimit = CPLAtoGIntBig(
774 0 : osFilename.substr(nPos + strlen("&limit=")).c_str());
775 0 : nMaxItems = std::max(nMaxItems, nLimit);
776 : }
777 : }
778 :
779 42 : auto osCurFilename = osFilename;
780 42 : std::string osMethod = "GET";
781 42 : CPLJSONObject oHeaders;
782 42 : CPLJSONObject oBody;
783 21 : bool bMerge = false;
784 21 : int nLoops = 0;
785 3 : do
786 : {
787 24 : ++nLoops;
788 24 : if (nMaxItems > 0 && nLoops > nMaxItems)
789 : {
790 21 : break;
791 : }
792 :
793 24 : CPLJSONDocument oDoc;
794 :
795 47 : if (STARTS_WITH(osCurFilename, "http://") ||
796 23 : STARTS_WITH(osCurFilename, "https://"))
797 : {
798 : // Cf // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
799 1 : CPLStringList aosOptions;
800 2 : if (oBody.IsValid() &&
801 1 : oBody.GetType() == CPLJSONObject::Type::Object)
802 : {
803 1 : if (bMerge)
804 0 : CPLDebug("STACIT",
805 : "Ignoring 'merge' attribute from next link");
806 : const std::string osPostContent =
807 2 : oBody.Format(CPLJSONObject::PrettyFormat::Pretty);
808 1 : aosOptions.SetNameValue("POSTFIELDS", osPostContent.c_str());
809 : }
810 1 : aosOptions.SetNameValue("CUSTOMREQUEST", osMethod.c_str());
811 1 : CPLString osHeaders;
812 4 : if (!oHeaders.IsValid() ||
813 2 : oHeaders.GetType() != CPLJSONObject::Type::Object ||
814 2 : oHeaders["Content-Type"].ToString().empty())
815 : {
816 1 : osHeaders = "Content-Type: application/json";
817 : }
818 2 : if (oHeaders.IsValid() &&
819 1 : oHeaders.GetType() == CPLJSONObject::Type::Object)
820 : {
821 2 : for (const auto &obj : oHeaders.GetChildren())
822 : {
823 1 : osHeaders += "\r\n";
824 1 : osHeaders += obj.GetName();
825 1 : osHeaders += ": ";
826 1 : osHeaders += obj.ToString();
827 : }
828 : }
829 1 : aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
830 : CPLHTTPResult *psResult =
831 1 : CPLHTTPFetch(osCurFilename.c_str(), aosOptions.List());
832 1 : if (!psResult)
833 0 : return false;
834 1 : if (!psResult->pabyData)
835 : {
836 0 : CPLHTTPDestroyResult(psResult);
837 0 : return false;
838 : }
839 2 : const bool bOK = oDoc.LoadMemory(
840 1 : reinterpret_cast<const char *>(psResult->pabyData));
841 : // CPLDebug("STACIT", "Response: %s", reinterpret_cast<const char*>(psResult->pabyData));
842 1 : CPLHTTPDestroyResult(psResult);
843 1 : if (!bOK)
844 0 : return false;
845 : }
846 : else
847 : {
848 23 : if (!oDoc.Load(osCurFilename))
849 0 : return false;
850 : }
851 24 : const auto oRoot = oDoc.GetRoot();
852 48 : auto oFeatures = oRoot.GetArray("features");
853 24 : if (!oFeatures.IsValid())
854 : {
855 2 : if (oRoot.GetString("type") == "Feature")
856 : {
857 2 : oFeatures = CPLJSONArray();
858 2 : oFeatures.Add(oRoot);
859 : }
860 : else
861 : {
862 0 : CPLError(CE_Failure, CPLE_AppDefined, "Missing features");
863 0 : return false;
864 : }
865 : }
866 74 : for (const auto &oFeature : oFeatures)
867 : {
868 50 : nItemIter++;
869 50 : if (nMaxItems > 0 && nItemIter > nMaxItems)
870 : {
871 0 : break;
872 : }
873 :
874 100 : auto oStacExtensions = oFeature.GetArray("stac_extensions");
875 50 : if (!oStacExtensions.IsValid())
876 : {
877 0 : CPLDebug("STACIT",
878 : "Skipping Feature that lacks stac_extensions");
879 0 : continue;
880 : }
881 50 : bool bProjExtensionFound = false;
882 150 : for (const auto &oStacExtension : oStacExtensions)
883 : {
884 251 : if (oStacExtension.ToString() == "proj" ||
885 151 : oStacExtension.ToString().find(
886 : "https://stac-extensions.github.io/projection/") == 0)
887 : {
888 50 : bProjExtensionFound = true;
889 50 : break;
890 : }
891 : }
892 50 : if (!bProjExtensionFound)
893 : {
894 0 : CPLDebug(
895 : "STACIT",
896 : "Skipping Feature that lacks the 'proj' STAC extension");
897 0 : continue;
898 : }
899 :
900 100 : auto jAssets = oFeature["assets"];
901 100 : if (!jAssets.IsValid() ||
902 50 : jAssets.GetType() != CPLJSONObject::Type::Object)
903 : {
904 0 : CPLError(CE_Warning, CPLE_AppDefined,
905 : "Missing assets on a Feature");
906 0 : continue;
907 : }
908 :
909 100 : auto oProperties = oFeature["properties"];
910 100 : if (!oProperties.IsValid() ||
911 50 : oProperties.GetType() != CPLJSONObject::Type::Object)
912 : {
913 0 : CPLError(CE_Warning, CPLE_AppDefined,
914 : "Missing properties on a Feature");
915 0 : continue;
916 : }
917 :
918 100 : const auto osCollection = oFeature["collection"].ToString();
919 65 : if (!osFilteredCollection.empty() &&
920 15 : osFilteredCollection != osCollection)
921 8 : continue;
922 :
923 95 : for (const auto &jAsset : jAssets.GetChildren())
924 : {
925 53 : const auto osAssetName = jAsset.GetName();
926 53 : if (!osFilteredAsset.empty() && osFilteredAsset != osAssetName)
927 8 : continue;
928 :
929 45 : ParseAsset(jAsset, oProperties, osCollection, osFilteredCRS,
930 : oMapCollection);
931 : }
932 : }
933 24 : if (nMaxItems > 0 && nItemIter >= nMaxItems)
934 : {
935 1 : if (!bMaxItemsSpecified)
936 : {
937 0 : CPLError(CE_Warning, CPLE_AppDefined,
938 : "Maximum number of items (" CPL_FRMT_GIB
939 : ") allowed to be retrieved has been hit",
940 : nMaxItems);
941 : }
942 : else
943 : {
944 1 : CPLDebug("STACIT",
945 : "Maximum number of items (" CPL_FRMT_GIB
946 : ") allowed to be retrieved has been hit",
947 : nMaxItems);
948 : }
949 1 : break;
950 : }
951 :
952 : // Follow next link
953 : // Cf https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search#pagination
954 46 : const auto oLinks = oRoot.GetArray("links");
955 23 : if (!oLinks.IsValid())
956 20 : break;
957 6 : std::string osNewFilename;
958 6 : for (const auto &oLink : oLinks)
959 : {
960 6 : const auto osType = oLink["type"].ToString();
961 6 : if (oLink["rel"].ToString() == "next" &&
962 3 : (osType.empty() || osType == "application/geo+json"))
963 : {
964 3 : osMethod = oLink.GetString("method", "GET");
965 3 : osNewFilename = oLink["href"].ToString();
966 3 : oHeaders = oLink["headers"];
967 3 : oBody = oLink["body"];
968 3 : bMerge = oLink.GetBool("merge", false);
969 3 : if (osType == "application/geo+json")
970 : {
971 0 : break;
972 : }
973 : }
974 : }
975 6 : if (!osNewFilename.empty() &&
976 3 : (osNewFilename != osCurFilename ||
977 0 : (oBody.IsValid() &&
978 0 : oBody.GetType() == CPLJSONObject::Type::Object)))
979 : {
980 3 : osCurFilename = osNewFilename;
981 : }
982 : else
983 0 : osCurFilename.clear();
984 3 : } while (!osCurFilename.empty());
985 :
986 21 : if (oMapCollection.empty())
987 : {
988 2 : CPLError(CE_Failure, CPLE_AppDefined, "No compatible asset found");
989 2 : return false;
990 : }
991 :
992 37 : if (oMapCollection.size() > 1 ||
993 37 : oMapCollection.begin()->second.assets.size() > 1 ||
994 37 : oMapCollection.begin()->second.assets.begin()->second.assets.size() > 1)
995 : {
996 : // If there's more than one asset type or more than one SRS, expose
997 : // subdatasets.
998 1 : SetSubdatasets(osFilename, oMapCollection);
999 1 : return true;
1000 : }
1001 : else
1002 : {
1003 18 : return SetupDataset(poOpenInfo, osFilename, oMapCollection);
1004 : }
1005 : }
1006 :
1007 : /************************************************************************/
1008 : /* OpenStatic() */
1009 : /************************************************************************/
1010 :
1011 21 : GDALDataset *STACITDataset::OpenStatic(GDALOpenInfo *poOpenInfo)
1012 : {
1013 21 : if (!Identify(poOpenInfo))
1014 0 : return nullptr;
1015 42 : auto poDS = std::make_unique<STACITDataset>();
1016 21 : if (!poDS->Open(poOpenInfo))
1017 2 : return nullptr;
1018 19 : return poDS.release();
1019 : }
1020 :
1021 : /************************************************************************/
1022 : /* GDALRegister_STACIT() */
1023 : /************************************************************************/
1024 :
1025 2024 : void GDALRegister_STACIT()
1026 :
1027 : {
1028 2024 : if (GDALGetDriverByName("STACIT") != nullptr)
1029 283 : return;
1030 :
1031 1741 : GDALDriver *poDriver = new GDALDriver();
1032 :
1033 1741 : poDriver->SetDescription("STACIT");
1034 1741 : poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
1035 1741 : poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
1036 1741 : "Spatio-Temporal Asset Catalog Items");
1037 1741 : poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/stacit.html");
1038 1741 : poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
1039 1741 : poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
1040 1741 : poDriver->SetMetadataItem(
1041 : GDAL_DMD_OPENOPTIONLIST,
1042 : "<OpenOptionList>"
1043 : " <Option name='MAX_ITEMS' type='int' default='1000' "
1044 : "description='Maximum number of items fetched. 0=unlimited'/>"
1045 : " <Option name='COLLECTION' type='string' "
1046 : "description='Name of collection to filter items'/>"
1047 : " <Option name='ASSET' type='string' "
1048 : "description='Name of asset to filter items'/>"
1049 : " <Option name='CRS' type='string' "
1050 : "description='Name of CRS to filter items'/>"
1051 : " <Option name='RESOLUTION' type='string-select' default='AVERAGE' "
1052 : "description='Strategy to use to determine dataset resolution'>"
1053 : " <Value>AVERAGE</Value>"
1054 : " <Value>HIGHEST</Value>"
1055 : " <Value>LOWEST</Value>"
1056 : " </Option>"
1057 : " <Option name='OVERLAP_STRATEGY' type='string-select' "
1058 : "default='REMOVE_IF_NO_NODATA' "
1059 : "description='Strategy to use when some sources are fully "
1060 : "covered by others'>"
1061 : " <Value>REMOVE_IF_NO_NODATA</Value>"
1062 : " <Value>USE_ALL</Value>"
1063 : " <Value>USE_MOST_RECENT</Value>"
1064 : " </Option>"
1065 1741 : "</OpenOptionList>");
1066 :
1067 1741 : poDriver->pfnOpen = STACITDataset::OpenStatic;
1068 1741 : poDriver->pfnIdentify = STACITDataset::Identify;
1069 :
1070 1741 : GetGDALDriverManager()->RegisterDriver(poDriver);
1071 : }
|