Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: Earth Engine Data API Images driver
4 : * Purpose: Earth Engine Data API Images driver
5 : * Author: Even Rouault, even dot rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2017-2018, Planet Labs
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "cpl_http.h"
14 : #include "eeda.h"
15 : #include "ogrlibjsonutils.h"
16 :
17 : #include <stdlib.h>
18 : #include <limits>
19 :
20 : std::vector<EEDAIBandDesc>
21 10 : BuildBandDescArray(json_object *poBands,
22 : std::map<CPLString, CPLString> &oMapCodeToWKT)
23 : {
24 10 : const auto nBandCount = json_object_array_length(poBands);
25 10 : std::vector<EEDAIBandDesc> aoBandDesc;
26 :
27 34 : for (auto i = decltype(nBandCount){0}; i < nBandCount; i++)
28 : {
29 24 : json_object *poBand = json_object_array_get_idx(poBands, i);
30 48 : if (poBand == nullptr ||
31 24 : json_object_get_type(poBand) != json_type_object)
32 0 : continue;
33 :
34 24 : json_object *poId = CPL_json_object_object_get(poBand, "id");
35 24 : const char *pszBandId = json_object_get_string(poId);
36 24 : if (pszBandId == nullptr)
37 0 : continue;
38 :
39 : json_object *poDataType =
40 24 : CPL_json_object_object_get(poBand, "dataType");
41 48 : if (poDataType == nullptr ||
42 24 : json_object_get_type(poDataType) != json_type_object)
43 : {
44 0 : continue;
45 : }
46 :
47 : json_object *poPrecision =
48 24 : CPL_json_object_object_get(poDataType, "precision");
49 24 : const char *pszPrecision = json_object_get_string(poPrecision);
50 24 : if (pszPrecision == nullptr)
51 0 : continue;
52 24 : GDALDataType eDT = GDT_Byte;
53 24 : if (EQUAL(pszPrecision, "INT"))
54 : {
55 : json_object *poRange =
56 24 : CPL_json_object_object_get(poDataType, "range");
57 24 : if (poRange && json_object_get_type(poRange) == json_type_object)
58 : {
59 24 : int nMin = 0;
60 24 : int nMax = 0;
61 24 : json_object *poMin = CPL_json_object_object_get(poRange, "min");
62 24 : if (poMin)
63 : {
64 0 : nMin = json_object_get_int(poMin);
65 : }
66 24 : json_object *poMax = CPL_json_object_object_get(poRange, "max");
67 24 : if (poMax)
68 : {
69 24 : nMax = json_object_get_int(poMax);
70 : }
71 :
72 24 : if (nMin == -128 && nMax == 127)
73 : {
74 0 : eDT = GDT_Int8;
75 : }
76 24 : else if (nMin < std::numeric_limits<GInt16>::min())
77 : {
78 0 : eDT = GDT_Int32;
79 : }
80 24 : else if (nMax > std::numeric_limits<GUInt16>::max())
81 : {
82 0 : eDT = GDT_UInt32;
83 : }
84 24 : else if (nMin < 0)
85 : {
86 0 : eDT = GDT_Int16;
87 : }
88 24 : else if (nMax > std::numeric_limits<GByte>::max())
89 : {
90 19 : eDT = GDT_UInt16;
91 : }
92 : }
93 : }
94 0 : else if (EQUAL(pszPrecision, "FLOAT"))
95 : {
96 0 : eDT = GDT_Float32;
97 : }
98 0 : else if (EQUAL(pszPrecision, "DOUBLE"))
99 : {
100 0 : eDT = GDT_Float64;
101 : }
102 : else
103 : {
104 0 : CPLError(CE_Warning, CPLE_NotSupported,
105 : "Unhandled dataType %s for band %s", pszPrecision,
106 : pszBandId);
107 0 : continue;
108 : }
109 :
110 24 : json_object *poGrid = CPL_json_object_object_get(poBand, "grid");
111 48 : if (poGrid == nullptr ||
112 24 : json_object_get_type(poGrid) != json_type_object)
113 : {
114 0 : continue;
115 : }
116 :
117 24 : CPLString osWKT;
118 : // Cf https://developers.google.com/earth-engine/reference/rest/v1alpha/PixelGrid
119 24 : json_object *poCrs = CPL_json_object_object_get(poGrid, "crsCode");
120 24 : if (poCrs == nullptr)
121 0 : poCrs = CPL_json_object_object_get(poGrid, "crsWkt");
122 24 : if (poCrs ==
123 : nullptr) // "wkt" must come from a preliminary version of the API
124 0 : poCrs = CPL_json_object_object_get(poGrid, "wkt");
125 24 : OGRSpatialReference oSRS;
126 24 : if (poCrs)
127 : {
128 24 : const char *pszStr = json_object_get_string(poCrs);
129 24 : if (pszStr == nullptr)
130 0 : continue;
131 24 : if (STARTS_WITH(pszStr, "SR-ORG:"))
132 : {
133 : // For EEDA:MCD12Q1 for example
134 : pszStr =
135 0 : CPLSPrintf("http://spatialreference.org/ref/sr-org/%s/",
136 : pszStr + strlen("SR-ORG:"));
137 : }
138 :
139 : std::map<CPLString, CPLString>::const_iterator oIter =
140 24 : oMapCodeToWKT.find(pszStr);
141 24 : if (oIter != oMapCodeToWKT.end())
142 : {
143 15 : osWKT = oIter->second;
144 : }
145 9 : else if (oSRS.SetFromUserInput(pszStr) != OGRERR_NONE)
146 : {
147 0 : CPLError(CE_Warning, CPLE_AppDefined, "Unrecognized crs: %s",
148 : pszStr);
149 0 : oMapCodeToWKT[pszStr] = "";
150 : }
151 : else
152 : {
153 9 : char *pszWKT = nullptr;
154 9 : oSRS.exportToWkt(&pszWKT);
155 9 : if (pszWKT != nullptr)
156 9 : osWKT = pszWKT;
157 9 : CPLFree(pszWKT);
158 9 : oMapCodeToWKT[pszStr] = osWKT;
159 : }
160 : }
161 :
162 : json_object *poAT =
163 24 : CPL_json_object_object_get(poGrid, "affineTransform");
164 24 : if (poAT == nullptr || json_object_get_type(poAT) != json_type_object)
165 : {
166 0 : continue;
167 : }
168 : std::vector<double> adfGeoTransform{
169 48 : json_object_get_double(
170 24 : CPL_json_object_object_get(poAT, "translateX")),
171 24 : json_object_get_double(CPL_json_object_object_get(poAT, "scaleX")),
172 24 : json_object_get_double(CPL_json_object_object_get(poAT, "shearX")),
173 48 : json_object_get_double(
174 24 : CPL_json_object_object_get(poAT, "translateY")),
175 24 : json_object_get_double(CPL_json_object_object_get(poAT, "shearY")),
176 24 : json_object_get_double(CPL_json_object_object_get(poAT, "scaleY")),
177 144 : };
178 :
179 : json_object *poDimensions =
180 24 : CPL_json_object_object_get(poGrid, "dimensions");
181 48 : if (poDimensions == nullptr ||
182 24 : json_object_get_type(poDimensions) != json_type_object)
183 : {
184 0 : continue;
185 : }
186 : json_object *poWidth =
187 24 : CPL_json_object_object_get(poDimensions, "width");
188 24 : int nWidth = json_object_get_int(poWidth);
189 : json_object *poHeight =
190 24 : CPL_json_object_object_get(poDimensions, "height");
191 24 : int nHeight = json_object_get_int(poHeight);
192 :
193 : #if 0
194 : if( poWidth == nullptr && poHeight == nullptr && poX == nullptr && poY == nullptr &&
195 : dfResX == 1.0 && dfResY == 1.0 )
196 : {
197 : // e.g. EEDAI:LT5_L1T_8DAY_EVI/19840109
198 : const char* pszAuthorityName = oSRS.GetAuthorityName(nullptr);
199 : const char* pszAuthorityCode = oSRS.GetAuthorityCode(nullptr);
200 : if( pszAuthorityName && pszAuthorityCode &&
201 : EQUAL(pszAuthorityName, "EPSG") &&
202 : EQUAL(pszAuthorityCode, "4326") )
203 : {
204 : dfX = -180;
205 : dfY = 90;
206 : nWidth = 1 << 30;
207 : nHeight = 1 << 29;
208 : dfResX = 360.0 / nWidth;
209 : dfResY = -dfResX;
210 : }
211 : }
212 : #endif
213 :
214 24 : if (nWidth <= 0 || nHeight <= 0)
215 : {
216 0 : CPLError(CE_Warning, CPLE_AppDefined,
217 : "Invalid width/height for band %s", pszBandId);
218 0 : continue;
219 : }
220 :
221 48 : EEDAIBandDesc oDesc;
222 24 : oDesc.osName = pszBandId;
223 24 : oDesc.osWKT = std::move(osWKT);
224 24 : oDesc.eDT = eDT;
225 24 : oDesc.adfGeoTransform = std::move(adfGeoTransform);
226 24 : oDesc.nWidth = nWidth;
227 24 : oDesc.nHeight = nHeight;
228 24 : aoBandDesc.emplace_back(std::move(oDesc));
229 : }
230 10 : return aoBandDesc;
231 : }
232 :
233 : /************************************************************************/
234 : /* GDALEEDABaseDataset() */
235 : /************************************************************************/
236 :
237 43 : GDALEEDABaseDataset::GDALEEDABaseDataset()
238 43 : : m_bMustCleanPersistent(false), m_nExpirationTime(0)
239 : {
240 43 : }
241 :
242 : /************************************************************************/
243 : /* ~GDALEEDABaseDataset() */
244 : /************************************************************************/
245 :
246 43 : GDALEEDABaseDataset::~GDALEEDABaseDataset()
247 : {
248 43 : if (m_bMustCleanPersistent)
249 : {
250 15 : char **papszOptions = CSLSetNameValue(nullptr, "CLOSE_PERSISTENT",
251 : CPLSPrintf("EEDAI:%p", this));
252 15 : CPLHTTPDestroyResult(CPLHTTPFetch(m_osBaseURL, papszOptions));
253 15 : CSLDestroy(papszOptions);
254 : }
255 43 : }
256 :
257 : /************************************************************************/
258 : /* ConvertPathToName() */
259 : /************************************************************************/
260 :
261 16 : CPLString GDALEEDABaseDataset::ConvertPathToName(const CPLString &path)
262 : {
263 16 : size_t end = path.find('/');
264 32 : CPLString folder = path.substr(0, end);
265 :
266 16 : if (folder == "users")
267 : {
268 2 : return "projects/earthengine-legacy/assets/" + path;
269 : }
270 15 : else if (folder != "projects")
271 : {
272 24 : return "projects/earthengine-public/assets/" + path;
273 : }
274 :
275 : // Find the start and end positions of the third segment, if it exists.
276 3 : int segment = 1;
277 3 : size_t start = 0;
278 8 : while (end != std::string::npos && segment < 3)
279 : {
280 5 : segment++;
281 5 : start = end + 1;
282 5 : end = path.find('/', start);
283 : }
284 :
285 3 : end = (end == std::string::npos) ? path.size() : end;
286 : // segment is 3 if path has at least 3 segments.
287 3 : if (folder == "projects" && segment == 3)
288 : {
289 : // If the first segment is "projects" and the third segment is "assets",
290 : // path is a name, so return as-is.
291 2 : if (path.substr(start, end - start) == "assets")
292 : {
293 1 : return path;
294 : }
295 : }
296 4 : return "projects/earthengine-legacy/assets/" + path;
297 : }
298 :
299 : /************************************************************************/
300 : /* GetBaseHTTPOptions() */
301 : /************************************************************************/
302 :
303 27 : char **GDALEEDABaseDataset::GetBaseHTTPOptions()
304 : {
305 27 : m_bMustCleanPersistent = true;
306 :
307 27 : char **papszOptions = nullptr;
308 : papszOptions =
309 27 : CSLAddString(papszOptions, CPLSPrintf("PERSISTENT=EEDAI:%p", this));
310 :
311 : // Strategy to get the Bearer Authorization value:
312 : // - if it is specified in the EEDA_BEARER config option, use it
313 : // - otherwise if EEDA_BEARER_FILE is specified, read it and use its content
314 : // - otherwise if GOOGLE_APPLICATION_CREDENTIALS is specified, read the
315 : // corresponding file to get the private key and client_email, to get a
316 : // bearer using OAuth2ServiceAccount method
317 : // - otherwise if EEDA_PRIVATE_KEY and EEDA_CLIENT_EMAIL are set, use them
318 : // to get a bearer using OAuth2ServiceAccount method
319 : // - otherwise if EEDA_PRIVATE_KEY_FILE and EEDA_CLIENT_EMAIL are set, use
320 : // them to get a bearer
321 :
322 54 : CPLString osBearer(CPLGetConfigOption("EEDA_BEARER", m_osBearer));
323 50 : if (osBearer.empty() ||
324 23 : (!m_osBearer.empty() && time(nullptr) > m_nExpirationTime))
325 : {
326 4 : CPLString osBearerFile(CPLGetConfigOption("EEDA_BEARER_FILE", ""));
327 4 : if (!osBearerFile.empty())
328 : {
329 0 : VSILFILE *fp = VSIFOpenL(osBearerFile, "rb");
330 0 : if (fp == nullptr)
331 : {
332 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
333 : osBearerFile.c_str());
334 : }
335 : else
336 : {
337 : char abyBuffer[512];
338 0 : size_t nRead = VSIFReadL(abyBuffer, 1, sizeof(abyBuffer), fp);
339 0 : osBearer.assign(abyBuffer, nRead);
340 0 : VSIFCloseL(fp);
341 : }
342 : }
343 : else
344 : {
345 4 : CPLString osPrivateKey(CPLGetConfigOption("EEDA_PRIVATE_KEY", ""));
346 : CPLString osClientEmail(
347 4 : CPLGetConfigOption("EEDA_CLIENT_EMAIL", ""));
348 :
349 4 : if (osPrivateKey.empty())
350 : {
351 : CPLString osPrivateKeyFile(
352 6 : CPLGetConfigOption("EEDA_PRIVATE_KEY_FILE", ""));
353 3 : if (!osPrivateKeyFile.empty())
354 : {
355 0 : VSILFILE *fp = VSIFOpenL(osPrivateKeyFile, "rb");
356 0 : if (fp == nullptr)
357 : {
358 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
359 : osPrivateKeyFile.c_str());
360 : }
361 : else
362 : {
363 : char *pabyBuffer =
364 0 : static_cast<char *>(CPLMalloc(32768));
365 0 : size_t nRead = VSIFReadL(pabyBuffer, 1, 32768, fp);
366 0 : osPrivateKey.assign(pabyBuffer, nRead);
367 0 : VSIFCloseL(fp);
368 0 : CPLFree(pabyBuffer);
369 : }
370 : }
371 : }
372 :
373 4 : CPLString osServiceAccountJson;
374 : const char *pszVSIPath =
375 4 : CSLFetchNameValue(papszOpenOptions, "VSI_PATH_FOR_AUTH");
376 4 : if (pszVSIPath)
377 : osServiceAccountJson = VSIGetPathSpecificOption(
378 0 : pszVSIPath, "GOOGLE_APPLICATION_CREDENTIALS", "");
379 4 : if (osServiceAccountJson.empty())
380 : osServiceAccountJson =
381 4 : CPLGetConfigOption("GOOGLE_APPLICATION_CREDENTIALS", "");
382 4 : if (!osServiceAccountJson.empty())
383 : {
384 1 : CPLJSONDocument oDoc;
385 1 : if (!oDoc.Load(osServiceAccountJson))
386 : {
387 0 : CSLDestroy(papszOptions);
388 0 : return nullptr;
389 : }
390 :
391 1 : osPrivateKey = oDoc.GetRoot().GetString("private_key");
392 1 : osPrivateKey.replaceAll("\\n", "\n");
393 1 : osClientEmail = oDoc.GetRoot().GetString("client_email");
394 : }
395 :
396 4 : char **papszMD = nullptr;
397 4 : if (!osPrivateKey.empty() && !osClientEmail.empty())
398 : {
399 2 : CPLDebug("EEDA", "Requesting Bearer token");
400 2 : osPrivateKey.replaceAll("\\n", "\n");
401 : // CPLDebug("EEDA", "Private key: %s", osPrivateKey.c_str());
402 2 : papszMD = GOA2GetAccessTokenFromServiceAccount(
403 : osPrivateKey, osClientEmail,
404 : "https://www.googleapis.com/auth/earthengine.readonly",
405 : nullptr, nullptr);
406 2 : if (papszMD == nullptr)
407 : {
408 0 : CSLDestroy(papszOptions);
409 0 : return nullptr;
410 : }
411 : }
412 : // Some Travis-CI workers are GCE machines, and for some tests, we
413 : // don't want this code path to be taken. And on AppVeyor/Window, we
414 : // would also attempt a network access
415 4 : else if (!CPLTestBool(CPLGetConfigOption("CPL_GCE_SKIP", "NO")) &&
416 2 : CPLIsMachinePotentiallyGCEInstance())
417 : {
418 1 : papszMD = GOA2GetAccessTokenFromCloudEngineVM(nullptr);
419 : }
420 :
421 4 : if (papszMD)
422 : {
423 3 : osBearer = CSLFetchNameValueDef(papszMD, "access_token", "");
424 3 : m_osBearer = osBearer;
425 3 : m_nExpirationTime = CPLAtoGIntBig(
426 : CSLFetchNameValueDef(papszMD, "expires_in", "0"));
427 3 : if (m_nExpirationTime != 0)
428 3 : m_nExpirationTime += time(nullptr) - 10;
429 3 : CSLDestroy(papszMD);
430 : }
431 : else
432 : {
433 1 : CPLError(CE_Failure, CPLE_AppDefined,
434 : "Missing EEDA_BEARER, EEDA_BEARER_FILE or "
435 : "GOOGLE_APPLICATION_CREDENTIALS or "
436 : "EEDA_PRIVATE_KEY/EEDA_PRIVATE_KEY_FILE + "
437 : "EEDA_CLIENT_EMAIL config option or "
438 : "VSI_PATH_FOR_AUTH open option");
439 1 : CSLDestroy(papszOptions);
440 1 : return nullptr;
441 : }
442 : }
443 : }
444 26 : papszOptions = CSLAddString(
445 : papszOptions,
446 : CPLSPrintf("HEADERS=Authorization: Bearer %s", osBearer.c_str()));
447 :
448 26 : return papszOptions;
449 : }
450 :
451 : /* Add a small amount of random jitter to avoid cyclic server stampedes */
452 0 : static double EEDABackoffFactor(double base)
453 : {
454 : // coverity[dont_call]
455 0 : return base + rand() * 0.5 / RAND_MAX;
456 : }
457 :
458 : /************************************************************************/
459 : /* EEDAHTTPFetch() */
460 : /************************************************************************/
461 :
462 26 : CPLHTTPResult *EEDAHTTPFetch(const char *pszURL, char **papszOptions)
463 : {
464 : CPLHTTPResult *psResult;
465 26 : const int RETRY_COUNT = 4;
466 26 : double dfRetryDelay = 1.0;
467 26 : for (int i = 0; i <= RETRY_COUNT; i++)
468 : {
469 26 : psResult = CPLHTTPFetch(pszURL, papszOptions);
470 :
471 26 : if (psResult == nullptr)
472 0 : break;
473 26 : if (psResult->nDataLen != 0 && psResult->nStatus == 0 &&
474 26 : psResult->pszErrBuf == nullptr)
475 : {
476 : /* got a valid response */
477 26 : CPLErrorReset();
478 26 : break;
479 : }
480 : else
481 : {
482 0 : const char *pszErrorText =
483 0 : psResult->pszErrBuf ? psResult->pszErrBuf : "(null)";
484 :
485 : /* Get HTTP status code */
486 0 : int nHTTPStatus = -1;
487 0 : if (psResult->pszErrBuf != nullptr &&
488 0 : EQUALN(psResult->pszErrBuf,
489 : "HTTP error code : ", strlen("HTTP error code : ")))
490 : {
491 : nHTTPStatus =
492 0 : atoi(psResult->pszErrBuf + strlen("HTTP error code : "));
493 0 : if (psResult->pabyData)
494 0 : pszErrorText =
495 : reinterpret_cast<const char *>(psResult->pabyData);
496 : }
497 :
498 0 : if ((nHTTPStatus == 429 || nHTTPStatus == 500 ||
499 0 : (nHTTPStatus >= 502 && nHTTPStatus <= 504)) &&
500 : i < RETRY_COUNT)
501 : {
502 0 : CPLError(CE_Warning, CPLE_FileIO,
503 : "GET error when downloading %s, HTTP status=%d, "
504 : "retrying in %.2fs : %s",
505 : pszURL, nHTTPStatus, dfRetryDelay, pszErrorText);
506 0 : CPLHTTPDestroyResult(psResult);
507 0 : psResult = nullptr;
508 :
509 0 : CPLSleep(dfRetryDelay);
510 0 : dfRetryDelay *= EEDABackoffFactor(4);
511 : }
512 : else
513 : {
514 : break;
515 : }
516 : }
517 : }
518 :
519 26 : return psResult;
520 : }
|