Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: OGR
4 : * Purpose: Implements OGC API - Features (previously known as WFS3)
5 : * Author: Even Rouault, even dot rouault at spatialys dot com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2018-2019, Even Rouault <even dot rouault at spatialys dot com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "ogrsf_frmts.h"
14 : #include "cpl_conv.h"
15 : #include "cpl_minixml.h"
16 : #include "cpl_http.h"
17 : #include "ogr_swq.h"
18 : #include "parsexsd.h"
19 :
20 : #include <algorithm>
21 : #include <memory>
22 : #include <vector>
23 : #include <set>
24 :
25 : // g++ -Wshadow -Wextra -std=c++11 -fPIC -g -Wall
26 : // ogr/ogrsf_frmts/wfs/ogroapif*.cpp -shared -o ogr_OAPIF.so -Iport -Igcore
27 : // -Iogr -Iogr/ogrsf_frmts -Iogr/ogrsf_frmts/gml -Iogr/ogrsf_frmts/wfs -L.
28 : // -lgdal
29 :
30 : extern "C" void RegisterOGROAPIF();
31 :
32 : #define MEDIA_TYPE_OAPI_3_0 "application/vnd.oai.openapi+json;version=3.0"
33 : #define MEDIA_TYPE_OAPI_3_0_ALT "application/openapi+json;version=3.0"
34 : #define MEDIA_TYPE_JSON "application/json"
35 : #define MEDIA_TYPE_GEOJSON "application/geo+json"
36 : #define MEDIA_TYPE_TEXT_XML "text/xml"
37 : #define MEDIA_TYPE_APPLICATION_XML "application/xml"
38 : #define MEDIA_TYPE_JSON_SCHEMA "application/schema+json"
39 :
40 : constexpr const char *OGC_CRS84_WKT =
41 : "GEOGCRS[\"WGS 84 (CRS84)\",ENSEMBLE[\"World Geodetic System 1984 "
42 : "ensemble\",MEMBER[\"World Geodetic System 1984 "
43 : "(Transit)\"],MEMBER[\"World Geodetic System 1984 (G730)\"],MEMBER[\"World "
44 : "Geodetic System 1984 (G873)\"],MEMBER[\"World Geodetic System 1984 "
45 : "(G1150)\"],MEMBER[\"World Geodetic System 1984 (G1674)\"],MEMBER[\"World "
46 : "Geodetic System 1984 (G1762)\"],MEMBER[\"World Geodetic System 1984 "
47 : "(G2139)\"],ELLIPSOID[\"WGS "
48 : "84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ENSEMBLEACCURACY[2.0]]"
49 : ",PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS["
50 : "ellipsoidal,2],AXIS[\"geodetic longitude "
51 : "(Lon)\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS["
52 : "\"geodetic latitude "
53 : "(Lat)\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],USAGE["
54 : "SCOPE[\"Not "
55 : "known.\"],AREA[\"World.\"],BBOX[-90,-180,90,180]],ID[\"OGC\",\"CRS84\"]]";
56 :
57 : /************************************************************************/
58 : /* OGROAPIFDataset */
59 : /************************************************************************/
60 : class OGROAPIFLayer;
61 :
62 : class OGROAPIFDataset final : public GDALDataset
63 : {
64 : friend class OGROAPIFLayer;
65 :
66 : bool m_bMustCleanPersistent = false;
67 :
68 : // Server base URL. Like "https://example.com"
69 : // Relative links are relative to it
70 : CPLString m_osServerBaseURL{};
71 :
72 : // Service base URL. Like "https://example.com/ogcapi"
73 : CPLString m_osRootURL;
74 :
75 : CPLString m_osUserQueryParams;
76 : CPLString m_osUserPwd;
77 : int m_nPageSize = 1000;
78 : int m_nInitialRequestPageSize = 20;
79 : bool m_bPageSizeSetFromOpenOptions = false;
80 : std::vector<std::unique_ptr<OGRLayer>> m_apoLayers;
81 : std::string m_osAskedCRS{};
82 : OGRSpatialReference m_oAskedCRS{};
83 : bool m_bAskedCRSIsRequired = false;
84 : bool m_bServerFeaturesAxisOrderGISFriendly = false;
85 :
86 : bool m_bAPIDocLoaded = false;
87 : CPLJSONDocument m_oAPIDoc;
88 :
89 : bool m_bLandingPageDocLoaded = false;
90 : CPLJSONDocument m_oLandingPageDoc;
91 :
92 : bool m_bIgnoreSchema = false;
93 :
94 : std::string m_osDateTime{};
95 :
96 : bool Download(const CPLString &osURL, const char *pszAccept,
97 : CPLString &osResult, CPLString &osContentType,
98 : CPLStringList *paosHeaders = nullptr);
99 :
100 : bool DownloadJSon(const CPLString &osURL, CPLJSONDocument &oDoc,
101 : const char *pszAccept = MEDIA_TYPE_GEOJSON
102 : ", " MEDIA_TYPE_JSON,
103 : CPLStringList *paosHeaders = nullptr);
104 :
105 : bool LoadJSONCollection(const CPLJSONObject &oCollection,
106 : const CPLJSONArray &oGlobalCRSList);
107 : bool LoadJSONCollections(const CPLString &osResultIn,
108 : const std::string &osCollectionsURL);
109 :
110 : /**
111 : * Determines the page size by making a call to the API endpoint to get the server's
112 : * default and max limits for the collection items specified by itemsUrl
113 : */
114 : void DeterminePageSizeFromAPI(const std::string &itemsUrl);
115 :
116 : public:
117 41 : OGROAPIFDataset() = default;
118 : ~OGROAPIFDataset();
119 :
120 44 : int GetLayerCount() override
121 : {
122 44 : return static_cast<int>(m_apoLayers.size());
123 : }
124 :
125 : OGRLayer *GetLayer(int idx) override;
126 :
127 : bool Open(GDALOpenInfo *);
128 : const CPLJSONDocument &GetAPIDoc(std::string &osURLOut);
129 : const CPLJSONDocument &GetLandingPageDoc(std::string &osURLOut);
130 :
131 : CPLString ResolveURL(const CPLString &osURL,
132 : const std::string &osRequestURL) const;
133 : };
134 :
135 : /************************************************************************/
136 : /* OGROAPIFLayer */
137 : /************************************************************************/
138 :
139 : class OGROAPIFLayer final : public OGRLayer
140 : {
141 : OGROAPIFDataset *m_poDS = nullptr;
142 : OGRFeatureDefn *m_poFeatureDefn = nullptr;
143 : bool m_bIsGeographicCRS = false;
144 : bool m_bCRSHasGISFriendlyOrder = false;
145 : bool m_bHasEmittedContentCRSWarning = false;
146 : bool m_bHasEmittedJsonCRWarning = false;
147 : std::string m_osActiveCRS{};
148 : CPLString m_osURL;
149 : CPLString m_osPath;
150 : OGREnvelope m_oExtent;
151 : OGREnvelope m_oOriginalExtent;
152 : OGRSpatialReference m_oOriginalExtentCRS;
153 : bool m_bFeatureDefnEstablished = false;
154 : std::unique_ptr<GDALDataset> m_poUnderlyingDS;
155 : OGRLayer *m_poUnderlyingLayer = nullptr;
156 : GIntBig m_nFID = 1;
157 : CPLString m_osGetURL;
158 : CPLString m_osAttributeFilter;
159 : CPLString m_osGetID;
160 : std::vector<std::string> m_oSupportedCRSList{};
161 : OGRLayer::GetSupportedSRSListRetType m_apoSupportedCRSList{};
162 : bool m_bFilterMustBeClientSideEvaluated = false;
163 : bool m_bGotQueryableAttributes = false;
164 : std::set<CPLString> m_aoSetQueryableAttributes;
165 : bool m_bHasCQLText = false;
166 : // https://github.com/tschaub/ogcapi-features/blob/json-array-expression/extensions/cql/jfe/readme.md
167 : bool m_bHasJSONFilterExpression = false;
168 : GIntBig m_nTotalFeatureCount = -1;
169 : bool m_bHasIntIdMember = false;
170 : bool m_bHasStringIdMember = false;
171 : std::vector<std::unique_ptr<OGRFieldDefn>> m_apoFieldsFromSchema{};
172 : CPLString m_osDescribedByURL{};
173 : CPLString m_osDescribedByType{};
174 : bool m_bDescribedByIsXML = false;
175 : CPLString m_osQueryablesURL{};
176 : std::vector<std::string> m_aosItemAssetNames{}; // STAC specific
177 : CPLJSONDocument m_oCurDoc{};
178 : int m_iFeatureInPage = 0;
179 :
180 : void EstablishFeatureDefn();
181 : OGRFeature *GetNextRawFeature();
182 : CPLString AddFilters(const CPLString &osURL);
183 : CPLString BuildFilter(const swq_expr_node *poNode);
184 : CPLString BuildFilterCQLText(const swq_expr_node *poNode);
185 : CPLString BuildFilterJSONFilterExpr(const swq_expr_node *poNode);
186 : bool SupportsResultTypeHits();
187 : void GetQueryableAttributes();
188 : void GetSchema();
189 : void ComputeExtent();
190 :
191 : public:
192 : OGROAPIFLayer(OGROAPIFDataset *poDS, const CPLString &osName,
193 : const CPLJSONArray &oBBOX, const std::string &osBBOXCrs,
194 : std::vector<std::string> &&oCRSList,
195 : const std::string &osActiveCRS, double dfCoordinateEpoch,
196 : const CPLJSONArray &oLinks);
197 :
198 : ~OGROAPIFLayer();
199 :
200 : void SetItemAssets(const CPLJSONObject &oItemAssets);
201 :
202 11 : const char *GetName() override
203 : {
204 11 : return GetDescription();
205 : }
206 :
207 : OGRFeatureDefn *GetLayerDefn() override;
208 : void ResetReading() override;
209 : OGRFeature *GetNextFeature() override;
210 : OGRFeature *GetFeature(GIntBig) override;
211 : int TestCapability(const char *) override;
212 : GIntBig GetFeatureCount(int bForce = FALSE) override;
213 : OGRErr GetExtent(OGREnvelope *psExtent, int bForce = TRUE) override;
214 :
215 9 : OGRErr GetExtent(int iGeomField, OGREnvelope *psExtent, int bForce) override
216 : {
217 9 : return OGRLayer::GetExtent(iGeomField, psExtent, bForce);
218 : }
219 :
220 : void SetSpatialFilter(OGRGeometry *poGeom) override;
221 :
222 0 : void SetSpatialFilter(int iGeomField, OGRGeometry *poGeom) override
223 : {
224 0 : OGRLayer::SetSpatialFilter(iGeomField, poGeom);
225 0 : }
226 :
227 : OGRErr SetAttributeFilter(const char *pszQuery) override;
228 :
229 : const OGRLayer::GetSupportedSRSListRetType &
230 : GetSupportedSRSList(int iGeomField) override;
231 : OGRErr SetActiveSRS(int iGeomField,
232 : const OGRSpatialReference *poSRS) override;
233 : };
234 :
235 : /************************************************************************/
236 : /* CheckContentType() */
237 : /************************************************************************/
238 :
239 : // We may ask for "application/openapi+json;version=3.0"
240 : // and the server returns "application/openapi+json; charset=utf-8; version=3.0"
241 365 : static bool CheckContentType(const char *pszGotContentType,
242 : const char *pszExpectedContentType)
243 : {
244 730 : CPLStringList aosGotTokens(CSLTokenizeString2(pszGotContentType, "; ", 0));
245 : CPLStringList aosExpectedTokens(
246 730 : CSLTokenizeString2(pszExpectedContentType, "; ", 0));
247 622 : for (int i = 0; i < aosExpectedTokens.size(); i++)
248 : {
249 366 : bool bFound = false;
250 483 : for (int j = 0; j < aosGotTokens.size(); j++)
251 : {
252 374 : if (EQUAL(aosExpectedTokens[i], aosGotTokens[j]))
253 : {
254 257 : bFound = true;
255 257 : break;
256 : }
257 : }
258 366 : if (!bFound)
259 109 : return false;
260 : }
261 256 : return true;
262 : }
263 :
264 : /************************************************************************/
265 : /* ~OGROAPIFDataset() */
266 : /************************************************************************/
267 :
268 82 : OGROAPIFDataset::~OGROAPIFDataset()
269 : {
270 41 : if (m_bMustCleanPersistent)
271 : {
272 41 : char **papszOptions = CSLSetNameValue(nullptr, "CLOSE_PERSISTENT",
273 : CPLSPrintf("OAPIF:%p", this));
274 41 : CPLHTTPDestroyResult(CPLHTTPFetch(m_osRootURL, papszOptions));
275 41 : CSLDestroy(papszOptions);
276 : }
277 82 : }
278 :
279 : /************************************************************************/
280 : /* ResolveURL() */
281 : /************************************************************************/
282 :
283 : // Resolve relative links and re-inject authentication elements.
284 : // If source URL is https://user:pwd@server.com/bla
285 : // and link only contains https://server.com/bla, then insert
286 : // into it user:pwd
287 20 : CPLString OGROAPIFDataset::ResolveURL(const CPLString &osURL,
288 : const std::string &osRequestURL) const
289 : {
290 20 : const auto CleanURL = [](const std::string &osStr)
291 : {
292 20 : std::string osRet(osStr);
293 20 : const auto nPos = osRet.rfind('?');
294 20 : if (nPos != std::string::npos)
295 1 : osRet.resize(nPos);
296 20 : if (!osRet.empty() && osRet.back() == '/')
297 0 : osRet.pop_back();
298 20 : return osRet;
299 : };
300 :
301 20 : CPLString osRet(osURL);
302 : // Cf https://datatracker.ietf.org/doc/html/rfc3986#section-5.4
303 : // Partial implementation for usual cases...
304 : const std::string osRequestURLBase =
305 60 : CPLGetPath(CleanURL(osRequestURL).c_str());
306 20 : if (!osURL.empty() && osURL[0] == '/')
307 1 : osRet = m_osServerBaseURL + osURL;
308 19 : else if (osURL.size() > 2 && osURL[0] == '.' && osURL[1] == '/')
309 1 : osRet = osRequestURLBase + osURL.substr(1);
310 19 : else if (osURL.size() > 3 && osURL[0] == '.' && osURL[1] == '.' &&
311 1 : osURL[2] == '/')
312 : {
313 1 : std::string osModifiedRequestURL(osRequestURLBase);
314 3 : while (osRet.size() > 3 && osRet[0] == '.' && osRet[1] == '.' &&
315 1 : osRet[2] == '/')
316 : {
317 1 : osModifiedRequestURL = CPLGetPath(osModifiedRequestURL.c_str());
318 1 : osRet = osRet.substr(3);
319 : }
320 1 : osRet = osModifiedRequestURL + "/" + osRet;
321 : }
322 17 : else if (!STARTS_WITH(osURL.c_str(), "http://") &&
323 18 : !STARTS_WITH(osURL.c_str(), "https://") &&
324 1 : !STARTS_WITH(osURL.c_str(), "file://"))
325 : {
326 1 : osRet = osRequestURLBase + "/" + osURL;
327 : }
328 :
329 20 : const auto nArobaseInURLPos = m_osServerBaseURL.find('@');
330 40 : if (!osRet.empty() && STARTS_WITH(m_osServerBaseURL, "https://") &&
331 0 : STARTS_WITH(osRet, "https://") &&
332 40 : nArobaseInURLPos != std::string::npos &&
333 0 : osRet.find('@') == std::string::npos)
334 : {
335 : const auto nFirstSlashPos =
336 0 : m_osServerBaseURL.find('/', strlen("https://"));
337 0 : if (nFirstSlashPos == std::string::npos ||
338 : nFirstSlashPos > nArobaseInURLPos)
339 : {
340 : auto osUserPwd = m_osServerBaseURL.substr(
341 0 : strlen("https://"), nArobaseInURLPos - strlen("https://"));
342 : std::string osServer(
343 : nFirstSlashPos == std::string::npos
344 : ? m_osServerBaseURL.substr(nArobaseInURLPos + 1)
345 : : m_osServerBaseURL.substr(nArobaseInURLPos + 1,
346 : nFirstSlashPos -
347 0 : nArobaseInURLPos));
348 0 : if (STARTS_WITH(osRet, ("https://" + osServer).c_str()))
349 : {
350 0 : osRet = "https://" + osUserPwd + "@" +
351 0 : osRet.substr(strlen("https://"));
352 : }
353 : }
354 : }
355 40 : return osRet;
356 : }
357 :
358 : /************************************************************************/
359 : /* Download() */
360 : /************************************************************************/
361 :
362 194 : bool OGROAPIFDataset::Download(const CPLString &osURL, const char *pszAccept,
363 : CPLString &osResult, CPLString &osContentType,
364 : CPLStringList *paosHeaders)
365 : {
366 : #ifndef REMOVE_HACK
367 : VSIStatBufL sStatBuf;
368 194 : if (VSIStatL(osURL, &sStatBuf) == 0)
369 : {
370 0 : CPLDebug("OAPIF", "Reading %s", osURL.c_str());
371 0 : GByte *pabyRet = nullptr;
372 0 : if (VSIIngestFile(nullptr, osURL, &pabyRet, nullptr, -1))
373 : {
374 0 : osResult = reinterpret_cast<char *>(pabyRet);
375 0 : CPLFree(pabyRet);
376 : }
377 0 : return false;
378 : }
379 : #endif
380 194 : char **papszOptions = nullptr;
381 :
382 194 : if (pszAccept)
383 : {
384 : papszOptions =
385 193 : CSLSetNameValue(papszOptions, "HEADERS",
386 386 : (CPLString("Accept: ") + pszAccept).c_str());
387 : }
388 :
389 194 : if (!m_osUserPwd.empty())
390 : {
391 : papszOptions =
392 0 : CSLSetNameValue(papszOptions, "USERPWD", m_osUserPwd.c_str());
393 : }
394 194 : m_bMustCleanPersistent = true;
395 : papszOptions =
396 194 : CSLAddString(papszOptions, CPLSPrintf("PERSISTENT=OAPIF:%p", this));
397 388 : CPLString osURLWithQueryParameters(osURL);
398 15 : if (!m_osUserQueryParams.empty() &&
399 403 : osURL.find('?' + m_osUserQueryParams) == std::string::npos &&
400 205 : osURL.find('&' + m_osUserQueryParams) == std::string::npos)
401 : {
402 11 : if (osURL.find('?') == std::string::npos)
403 : {
404 6 : osURLWithQueryParameters += '?';
405 : }
406 : else
407 : {
408 5 : osURLWithQueryParameters += '&';
409 : }
410 11 : osURLWithQueryParameters += m_osUserQueryParams;
411 : }
412 : CPLHTTPResult *psResult =
413 194 : CPLHTTPFetch(osURLWithQueryParameters, papszOptions);
414 194 : CSLDestroy(papszOptions);
415 194 : if (!psResult)
416 0 : return false;
417 :
418 194 : if (psResult->pszErrBuf != nullptr)
419 : {
420 59 : std::string osErrorMsg(psResult->pszErrBuf);
421 59 : const char *pszData =
422 : reinterpret_cast<const char *>(psResult->pabyData);
423 59 : if (pszData)
424 : {
425 58 : osErrorMsg += ", ";
426 58 : osErrorMsg.append(pszData, CPLStrnlen(pszData, 1000));
427 : }
428 59 : CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMsg.c_str());
429 59 : CPLHTTPDestroyResult(psResult);
430 59 : return false;
431 : }
432 :
433 135 : if (psResult->pszContentType)
434 131 : osContentType = psResult->pszContentType;
435 :
436 : // Do not check content type if not specified
437 135 : bool bFoundExpectedContentType = pszAccept ? false : true;
438 :
439 135 : if (!bFoundExpectedContentType)
440 : {
441 : #ifndef REMOVE_HACK
442 : // cppcheck-suppress nullPointer
443 134 : if (strstr(pszAccept, "json"))
444 : {
445 134 : if (strstr(osURL, "raw.githubusercontent.com") &&
446 0 : strstr(osURL, ".json"))
447 : {
448 0 : bFoundExpectedContentType = true;
449 : }
450 264 : else if (psResult->pszContentType != nullptr &&
451 130 : (CheckContentType(psResult->pszContentType,
452 53 : MEDIA_TYPE_JSON) ||
453 53 : CheckContentType(psResult->pszContentType,
454 : MEDIA_TYPE_GEOJSON)))
455 : {
456 127 : bFoundExpectedContentType = true;
457 : }
458 : }
459 : #endif
460 :
461 : // cppcheck-suppress nullPointer
462 134 : if (strstr(pszAccept, "xml") && psResult->pszContentType != nullptr &&
463 0 : (CheckContentType(psResult->pszContentType, MEDIA_TYPE_TEXT_XML) ||
464 0 : CheckContentType(psResult->pszContentType,
465 : MEDIA_TYPE_APPLICATION_XML)))
466 : {
467 0 : bFoundExpectedContentType = true;
468 : }
469 :
470 : // cppcheck-suppress nullPointer
471 269 : if (strstr(pszAccept, MEDIA_TYPE_JSON_SCHEMA) &&
472 135 : psResult->pszContentType != nullptr &&
473 1 : (CheckContentType(psResult->pszContentType, MEDIA_TYPE_JSON) ||
474 1 : CheckContentType(psResult->pszContentType,
475 : MEDIA_TYPE_JSON_SCHEMA)))
476 : {
477 1 : bFoundExpectedContentType = true;
478 : }
479 :
480 76 : for (const char *pszMediaType : {
481 : MEDIA_TYPE_JSON,
482 : MEDIA_TYPE_GEOJSON,
483 : MEDIA_TYPE_OAPI_3_0,
484 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
485 : MEDIA_TYPE_OAPI_3_0_ALT,
486 : #endif
487 210 : })
488 : {
489 : // cppcheck-suppress nullPointer
490 596 : if (strstr(pszAccept, pszMediaType) &&
491 384 : psResult->pszContentType != nullptr &&
492 180 : CheckContentType(psResult->pszContentType, pszMediaType))
493 : {
494 128 : bFoundExpectedContentType = true;
495 128 : break;
496 : }
497 : }
498 : }
499 :
500 135 : if (!bFoundExpectedContentType)
501 : {
502 5 : CPLError(CE_Failure, CPLE_AppDefined, "Unexpected Content-Type: %s",
503 5 : psResult->pszContentType ? psResult->pszContentType
504 : : "(null)");
505 5 : CPLHTTPDestroyResult(psResult);
506 5 : return false;
507 : }
508 :
509 130 : if (psResult->pabyData == nullptr)
510 : {
511 0 : CPLError(CE_Failure, CPLE_AppDefined,
512 : "Empty content returned by server");
513 0 : CPLHTTPDestroyResult(psResult);
514 0 : return false;
515 : }
516 :
517 130 : if (paosHeaders)
518 : {
519 38 : *paosHeaders = CSLDuplicate(psResult->papszHeaders);
520 : }
521 :
522 130 : osResult = reinterpret_cast<const char *>(psResult->pabyData);
523 130 : CPLHTTPDestroyResult(psResult);
524 130 : return true;
525 : }
526 :
527 : /************************************************************************/
528 : /* DownloadJSon() */
529 : /************************************************************************/
530 :
531 150 : bool OGROAPIFDataset::DownloadJSon(const CPLString &osURL,
532 : CPLJSONDocument &oDoc, const char *pszAccept,
533 : CPLStringList *paosHeaders)
534 : {
535 300 : CPLString osResult;
536 300 : CPLString osContentType;
537 150 : if (!Download(osURL, pszAccept, osResult, osContentType, paosHeaders))
538 60 : return false;
539 90 : return oDoc.LoadMemory(osResult);
540 : }
541 :
542 : /************************************************************************/
543 : /* GetLandingPageDoc() */
544 : /************************************************************************/
545 :
546 30 : const CPLJSONDocument &OGROAPIFDataset::GetLandingPageDoc(std::string &osURLOut)
547 : {
548 30 : if (m_bLandingPageDocLoaded)
549 0 : return m_oLandingPageDoc;
550 30 : m_bLandingPageDocLoaded = true;
551 30 : osURLOut = m_osRootURL;
552 30 : CPL_IGNORE_RET_VAL(
553 30 : DownloadJSon(osURLOut, m_oLandingPageDoc, MEDIA_TYPE_JSON));
554 30 : return m_oLandingPageDoc;
555 : }
556 :
557 : /************************************************************************/
558 : /* GetAPIDoc() */
559 : /************************************************************************/
560 :
561 35 : const CPLJSONDocument &OGROAPIFDataset::GetAPIDoc(std::string &osURLOut)
562 : {
563 35 : if (m_bAPIDocLoaded)
564 5 : return m_oAPIDoc;
565 30 : m_bAPIDocLoaded = true;
566 :
567 : // Fetch the /api URL from the links of the landing page
568 60 : CPLString osAPIURL;
569 60 : std::string osLandingPageURL;
570 30 : const auto &oLandingPage = GetLandingPageDoc(osLandingPageURL);
571 30 : if (oLandingPage.GetRoot().IsValid())
572 : {
573 90 : const auto oLinks = oLandingPage.GetRoot().GetArray("links");
574 30 : if (oLinks.IsValid())
575 : {
576 10 : int nCountRelAPI = 0;
577 16 : for (int i = 0; i < oLinks.Size(); i++)
578 : {
579 16 : CPLJSONObject oLink = oLinks[i];
580 32 : if (!oLink.IsValid() ||
581 16 : oLink.GetType() != CPLJSONObject::Type::Object)
582 : {
583 0 : continue;
584 : }
585 32 : const auto osRel(oLink.GetString("rel"));
586 32 : const auto osType(oLink.GetString("type"));
587 16 : if (EQUAL(osRel.c_str(), "service-desc")
588 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
589 : // Needed for http://beta.fmi.fi/data/3/wfs/sofp
590 16 : || EQUAL(osRel.c_str(), "service")
591 : #endif
592 : )
593 : {
594 10 : nCountRelAPI++;
595 : osAPIURL =
596 10 : ResolveURL(oLink.GetString("href"), osLandingPageURL);
597 10 : if (osType == MEDIA_TYPE_OAPI_3_0
598 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
599 : // Needed for http://beta.fmi.fi/data/3/wfs/sofp
600 10 : || osType == MEDIA_TYPE_OAPI_3_0_ALT
601 : #endif
602 : )
603 : {
604 10 : nCountRelAPI = 1;
605 10 : break;
606 : }
607 : }
608 : }
609 10 : if (!osAPIURL.empty() && nCountRelAPI > 1)
610 : {
611 0 : osAPIURL.clear();
612 : }
613 : }
614 : }
615 :
616 30 : const char *pszAccept = MEDIA_TYPE_OAPI_3_0
617 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
618 : ", " MEDIA_TYPE_OAPI_3_0_ALT ", " MEDIA_TYPE_JSON
619 : #endif
620 : ;
621 :
622 30 : if (!osAPIURL.empty())
623 : {
624 10 : osURLOut = osAPIURL;
625 10 : CPL_IGNORE_RET_VAL(DownloadJSon(osAPIURL, m_oAPIDoc, pszAccept));
626 10 : return m_oAPIDoc;
627 : }
628 :
629 : #ifndef REMOVE_HACK
630 20 : CPLPushErrorHandler(CPLQuietErrorHandler);
631 40 : CPLString osURL(m_osRootURL + "/api");
632 20 : osURL = CPLGetConfigOption("OGR_WFS3_API_URL", osURL.c_str());
633 20 : bool bOK = DownloadJSon(osURL, m_oAPIDoc, pszAccept);
634 20 : CPLPopErrorHandler();
635 20 : CPLErrorReset();
636 20 : if (bOK)
637 : {
638 0 : return m_oAPIDoc;
639 : }
640 :
641 20 : osURLOut = m_osRootURL + "/api/";
642 20 : if (DownloadJSon(osURLOut, m_oAPIDoc, pszAccept))
643 : {
644 0 : return m_oAPIDoc;
645 : }
646 : #endif
647 20 : return m_oAPIDoc;
648 : }
649 :
650 : /************************************************************************/
651 : /* LoadJSONCollection() */
652 : /************************************************************************/
653 :
654 36 : bool OGROAPIFDataset::LoadJSONCollection(const CPLJSONObject &oCollection,
655 : const CPLJSONArray &oGlobalCRSList)
656 : {
657 36 : if (oCollection.GetType() != CPLJSONObject::Type::Object)
658 1 : return false;
659 :
660 : // As used by https://maps.ecere.com/ogcapi/collections?f=json
661 105 : const auto osLayerDataType = oCollection.GetString("layerDataType");
662 35 : if (osLayerDataType == "Raster" || osLayerDataType == "Coverage")
663 0 : return false;
664 :
665 105 : CPLString osName(oCollection.GetString("id"));
666 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
667 35 : if (osName.empty())
668 21 : osName = oCollection.GetString("name");
669 35 : if (osName.empty())
670 1 : osName = oCollection.GetString("collectionId");
671 : #endif
672 35 : if (osName.empty())
673 1 : return false;
674 :
675 102 : CPLString osTitle(oCollection.GetString("title"));
676 102 : CPLString osDescription(oCollection.GetString("description"));
677 102 : CPLJSONArray oBBOX = oCollection.GetArray("extent/spatial/bbox");
678 : #ifndef REMOVE_HACK_FOR_NLS_FINLAND_SERVICES
679 34 : if (!oBBOX.IsValid())
680 31 : oBBOX = oCollection.GetArray("extent/spatialExtent/bbox");
681 : #endif
682 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
683 34 : if (!oBBOX.IsValid())
684 31 : oBBOX = oCollection.GetArray("extent/spatial");
685 : #endif
686 102 : const std::string osBBOXCrs = oCollection.GetString("extent/spatial/crs");
687 :
688 : // Deal with CRS list
689 102 : const CPLJSONArray oCRSListOri = oCollection.GetArray("crs");
690 68 : std::vector<std::string> oCRSList;
691 68 : std::string osActiveCRS;
692 34 : double dfCoordinateEpoch = 0.0;
693 34 : if (oCRSListOri.IsValid())
694 : {
695 12 : std::set<std::string> oSetCRS;
696 40 : for (const auto &oCRS : oCRSListOri)
697 : {
698 28 : if (oCRS.ToString() == "#/crs")
699 : {
700 4 : if (!oGlobalCRSList.IsValid())
701 : {
702 0 : CPLError(CE_Failure, CPLE_AppDefined,
703 : "Collection %s refer to #/crs global CRS list, "
704 : "which is missing",
705 : osTitle.c_str());
706 : }
707 : else
708 : {
709 8 : for (const auto &oGlobalCRS : oGlobalCRSList)
710 : {
711 12 : const auto osCRS = oGlobalCRS.ToString();
712 4 : if (oSetCRS.find(osCRS) == oSetCRS.end())
713 : {
714 4 : oSetCRS.insert(osCRS);
715 4 : oCRSList.push_back(osCRS);
716 : }
717 : }
718 : }
719 : }
720 : else
721 : {
722 72 : const auto osCRS = oCRS.ToString();
723 24 : if (oSetCRS.find(osCRS) == oSetCRS.end())
724 : {
725 24 : oSetCRS.insert(osCRS);
726 24 : oCRSList.push_back(osCRS);
727 : }
728 : }
729 : }
730 :
731 12 : if (!m_oAskedCRS.IsEmpty())
732 : {
733 8 : for (const auto &osCRS : oCRSList)
734 : {
735 6 : OGRSpatialReference oSRS;
736 6 : if (oSRS.SetFromUserInput(
737 : osCRS.c_str(),
738 : OGRSpatialReference::
739 6 : SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
740 : OGRERR_NONE)
741 : {
742 6 : if (oSRS.IsSame(&m_oAskedCRS))
743 : {
744 2 : osActiveCRS = osCRS;
745 2 : break;
746 : }
747 : }
748 : }
749 4 : if (osActiveCRS.empty())
750 : {
751 2 : if (m_bAskedCRSIsRequired)
752 : {
753 1 : std::string osList;
754 3 : for (const auto &osCRS : oCRSList)
755 : {
756 2 : if (!osList.empty())
757 1 : osList += ", ";
758 2 : if (osCRS.find(
759 2 : "http://www.opengis.net/def/crs/EPSG/0/") == 0)
760 : osList +=
761 2 : "EPSG:" +
762 2 : osCRS.substr(strlen(
763 1 : "http://www.opengis.net/def/crs/EPSG/0/"));
764 1 : else if (osCRS.find("https://www.opengis.net/def/crs/"
765 1 : "EPSG/0/") == 0)
766 : osList +=
767 0 : "EPSG:" +
768 0 : osCRS.substr(strlen(
769 0 : "https://www.opengis.net/def/crs/EPSG/0/"));
770 1 : else if (osCRS.find("http://www.opengis.net/def/crs/"
771 1 : "OGC/1.3/") == 0)
772 : osList +=
773 2 : "OGC:" +
774 2 : osCRS.substr(strlen(
775 1 : "http://www.opengis.net/def/crs/OGC/1.3/"));
776 0 : else if (osCRS.find("https://www.opengis.net/def/crs/"
777 0 : "OGC/1.3/") == 0)
778 0 : osList += "OGC:" + osCRS.substr(strlen(
779 : "https://www.opengis.net/"
780 0 : "def/crs/OGC/1.3/"));
781 : else
782 0 : osList += osCRS;
783 : }
784 1 : CPLError(CE_Failure, CPLE_AppDefined,
785 : "CRS %s not found in list of CRS valid for "
786 : "collection %s. Available CRS are %s.",
787 : m_osAskedCRS.c_str(), osTitle.c_str(),
788 : osList.c_str());
789 1 : return false;
790 : }
791 : else
792 : {
793 1 : CPLDebug("OAPIF",
794 : "CRS %s not found in list of CRS valid for "
795 : "collection %s",
796 : m_osAskedCRS.c_str(), osTitle.c_str());
797 : }
798 : }
799 : }
800 : }
801 :
802 : // storageCRS is in the "OGC API - Features - Part 2: Coordinate Reference
803 : // Systems" extension
804 99 : const std::string osStorageCRS = oCollection.GetString("storageCrs");
805 : const double dfStorageCrsCoordinateEpoch =
806 33 : oCollection.GetDouble("storageCrsCoordinateEpoch");
807 33 : if (osActiveCRS.empty() || osActiveCRS == osStorageCRS)
808 : {
809 31 : osActiveCRS = osStorageCRS;
810 31 : dfCoordinateEpoch = dfStorageCrsCoordinateEpoch;
811 : }
812 :
813 99 : const auto oLinks = oCollection.GetArray("links");
814 : auto poLayer = std::make_unique<OGROAPIFLayer>(
815 33 : this, osName, oBBOX, osBBOXCrs, std::move(oCRSList), osActiveCRS,
816 66 : dfCoordinateEpoch, oLinks);
817 33 : if (!osTitle.empty())
818 2 : poLayer->SetMetadataItem("TITLE", osTitle.c_str());
819 33 : if (!osDescription.empty())
820 0 : poLayer->SetMetadataItem("DESCRIPTION", osDescription.c_str());
821 99 : auto oTemporalInterval = oCollection.GetArray("extent/temporal/interval");
822 33 : if (oTemporalInterval.IsValid() && oTemporalInterval.Size() == 1 &&
823 33 : oTemporalInterval[0].GetType() == CPLJSONObject::Type::Array)
824 : {
825 0 : auto oArray = oTemporalInterval[0].ToArray();
826 0 : if (oArray.Size() == 2)
827 : {
828 0 : if (oArray[0].GetType() == CPLJSONObject::Type::String)
829 : {
830 0 : poLayer->SetMetadataItem("TEMPORAL_INTERVAL_MIN",
831 0 : oArray[0].ToString().c_str());
832 : }
833 0 : if (oArray[1].GetType() == CPLJSONObject::Type::String)
834 : {
835 0 : poLayer->SetMetadataItem("TEMPORAL_INTERVAL_MAX",
836 0 : oArray[1].ToString().c_str());
837 : }
838 : }
839 : }
840 :
841 : // STAC specific
842 99 : auto oItemAssets = oCollection.GetObj("item_assets");
843 34 : if (oItemAssets.IsValid() &&
844 1 : oItemAssets.GetType() == CPLJSONObject::Type::Object)
845 : {
846 1 : poLayer->SetItemAssets(oItemAssets);
847 : }
848 :
849 33 : auto oJSONStr = oCollection.Format(CPLJSONObject::PrettyFormat::Pretty);
850 33 : char *apszMetadata[2] = {&oJSONStr[0], nullptr};
851 33 : poLayer->SetMetadata(apszMetadata, "json:metadata");
852 :
853 33 : m_apoLayers.emplace_back(std::move(poLayer));
854 33 : return true;
855 : }
856 :
857 : /************************************************************************/
858 : /* LoadJSONCollections() */
859 : /************************************************************************/
860 :
861 33 : bool OGROAPIFDataset::LoadJSONCollections(const CPLString &osResultIn,
862 : const std::string &osCollectionsURL)
863 : {
864 66 : std::string osParentURL(osCollectionsURL);
865 66 : CPLString osResult(osResultIn);
866 64 : while (!osResult.empty())
867 : {
868 34 : CPLJSONDocument oDoc;
869 34 : if (!oDoc.LoadMemory(osResult))
870 : {
871 1 : return false;
872 : }
873 33 : const auto &oRoot = oDoc.GetRoot();
874 66 : CPLJSONArray oCollections = oRoot.GetArray("collections");
875 33 : if (!oCollections.IsValid())
876 : {
877 2 : CPLError(CE_Failure, CPLE_AppDefined, "No collections array");
878 2 : return false;
879 : }
880 :
881 62 : const auto oGlobalCRSList = oRoot.GetArray("crs");
882 :
883 63 : for (int i = 0; i < oCollections.Size(); i++)
884 : {
885 32 : LoadJSONCollection(oCollections[i], oGlobalCRSList);
886 : }
887 :
888 31 : osResult.clear();
889 :
890 : // Paging is a (unspecified) extension to the core used by
891 : // https://{api_key}:@api.planet.com/analytics
892 62 : const auto oLinks = oRoot.GetArray("links");
893 31 : if (oLinks.IsValid())
894 : {
895 1 : CPLString osNextURL;
896 1 : int nCountRelNext = 0;
897 1 : for (int i = 0; i < oLinks.Size(); i++)
898 : {
899 1 : CPLJSONObject oLink = oLinks[i];
900 2 : if (!oLink.IsValid() ||
901 1 : oLink.GetType() != CPLJSONObject::Type::Object)
902 : {
903 0 : continue;
904 : }
905 1 : if (EQUAL(oLink.GetString("rel").c_str(), "next"))
906 : {
907 1 : osNextURL = oLink.GetString("href");
908 1 : nCountRelNext++;
909 2 : auto type = oLink.GetString("type");
910 1 : if (type == MEDIA_TYPE_GEOJSON || type == MEDIA_TYPE_JSON)
911 : {
912 1 : nCountRelNext = 1;
913 1 : break;
914 : }
915 : }
916 : }
917 1 : if (nCountRelNext == 1 && !osNextURL.empty())
918 : {
919 1 : CPLString osContentType;
920 1 : osNextURL = ResolveURL(osNextURL, osParentURL);
921 1 : osParentURL = osNextURL;
922 1 : if (!Download(osNextURL, MEDIA_TYPE_JSON, osResult,
923 : osContentType))
924 : {
925 0 : return false;
926 : }
927 : }
928 : }
929 : }
930 30 : return !m_apoLayers.empty();
931 : }
932 :
933 30 : void OGROAPIFDataset::DeterminePageSizeFromAPI(const std::string &itemsUrl)
934 : {
935 : // Try to get max limit from api
936 30 : int nMaximum{-1};
937 30 : int nDefault{-1};
938 : // Not sure if min should be considered
939 : //int nMinimum { -1 };
940 30 : std::string osAPIURL;
941 30 : const CPLJSONDocument &oDoc{GetAPIDoc(osAPIURL)};
942 30 : const auto &oRoot = oDoc.GetRoot();
943 :
944 30 : bool bFound{false};
945 :
946 : // limit from api document
947 30 : if (oRoot.IsValid())
948 : {
949 :
950 60 : const auto paths{oRoot.GetObj("paths")};
951 :
952 30 : if (paths.IsValid())
953 : {
954 :
955 10 : const auto pathName{itemsUrl.substr(m_osRootURL.length())};
956 10 : const auto path{paths.GetObj(pathName)};
957 :
958 10 : if (path.IsValid())
959 : {
960 :
961 16 : const auto parameters{path.GetArray("get/parameters")};
962 :
963 : // check $ref
964 15 : for (const auto ¶m : parameters)
965 : {
966 14 : const auto ref{param.GetString("$ref")};
967 7 : if (ref.find("limit") != std::string::npos)
968 : {
969 : // Examine ref
970 5 : if (ref.find("http") == 0 &&
971 5 : ref.find(".yml") == std::string::npos &&
972 1 : ref.find(".yaml") ==
973 : std::string::
974 : npos) // Remote document, skip yaml
975 : {
976 : // Only reinject auth if the URL matches
977 1 : auto limitUrl{ref.find(m_osRootURL) == 0
978 3 : ? ResolveURL(ref, osAPIURL)
979 2 : : ref};
980 1 : std::string fragment;
981 1 : const auto hashPos{limitUrl.find('#')};
982 1 : if (hashPos != std::string::npos)
983 : {
984 : // Remove leading #
985 1 : fragment = limitUrl.substr(hashPos + 1);
986 1 : limitUrl = limitUrl.substr(0, hashPos);
987 : }
988 1 : CPLString osResult;
989 1 : CPLString osContentType;
990 : // Do not limit accepted content-types, external resources may have any
991 1 : if (!Download(limitUrl, nullptr, osResult,
992 : osContentType))
993 : {
994 0 : CPLDebug("OAPIF",
995 : "Could not download OPENAPI $ref: %s",
996 : ref.c_str());
997 0 : return;
998 : }
999 :
1000 : // We cannot trust the content-type, try JSON (YAML not implemented)
1001 :
1002 : // Try JSON
1003 2 : CPLJSONDocument oLimitDoc;
1004 1 : if (oLimitDoc.LoadMemory(osResult))
1005 : {
1006 2 : const auto oLimitRoot{oLimitDoc.GetRoot()};
1007 1 : if (oLimitRoot.IsValid())
1008 : {
1009 : const auto oLimit{
1010 2 : oLimitRoot.GetObj(fragment)};
1011 1 : if (oLimit.IsValid())
1012 : {
1013 1 : nMaximum = oLimit.GetInteger(
1014 : "schema/maximum", -1);
1015 : //nMinimum = oLimit.GetInteger( "schema/minimum", -1 );
1016 1 : nDefault = oLimit.GetInteger(
1017 : "schema/default", -1);
1018 1 : bFound = true;
1019 : }
1020 : }
1021 : }
1022 : }
1023 3 : else if (ref.find('#') == 0) // Local ref
1024 : {
1025 6 : const auto oLimit{oRoot.GetObj(ref.substr(1))};
1026 3 : if (oLimit.IsValid())
1027 : {
1028 3 : nMaximum =
1029 3 : oLimit.GetInteger("schema/maximum", -1);
1030 : //nMinimum = oLimit.GetInteger( "schema/minimum", -1 );
1031 3 : nDefault =
1032 3 : oLimit.GetInteger("schema/default", -1);
1033 3 : bFound = true;
1034 : }
1035 : }
1036 : else
1037 : {
1038 0 : CPLDebug("OAPIF", "Could not open OPENAPI $ref: %s",
1039 : ref.c_str());
1040 : }
1041 : }
1042 : }
1043 : }
1044 : }
1045 : }
1046 :
1047 30 : if (bFound)
1048 : {
1049 : // Initially set to GDAL's default (1000)
1050 4 : int pageSize{m_nPageSize};
1051 4 : if (nDefault > 0 && nMaximum > 0)
1052 : {
1053 : // Use the default, but if it is below GDAL's default (1000), aim for 1000
1054 : // but clamp to the maximum limit
1055 4 : pageSize = std::min(std::max(pageSize, nDefault), nMaximum);
1056 : }
1057 0 : else if (nDefault > 0)
1058 0 : pageSize = std::max(pageSize, nDefault);
1059 0 : else if (nMaximum > 0)
1060 0 : pageSize = nMaximum;
1061 :
1062 4 : if (m_nPageSize != pageSize)
1063 : {
1064 2 : CPLDebug("OAPIF", "Page size set from OPENAPI schema: %d",
1065 : pageSize);
1066 2 : m_nPageSize = pageSize;
1067 : }
1068 : }
1069 : }
1070 :
1071 : /************************************************************************/
1072 : /* ConcatenateURLParts() */
1073 : /************************************************************************/
1074 :
1075 70 : static std::string ConcatenateURLParts(const std::string &osPart1,
1076 : const std::string &osPart2)
1077 : {
1078 70 : if (!osPart1.empty() && osPart1.back() == '/' && !osPart2.empty() &&
1079 0 : osPart2.front() == '/')
1080 : {
1081 0 : return osPart1.substr(0, osPart1.size() - 1) + osPart2;
1082 : }
1083 70 : return osPart1 + osPart2;
1084 : }
1085 :
1086 : /************************************************************************/
1087 : /* Open() */
1088 : /************************************************************************/
1089 :
1090 41 : bool OGROAPIFDataset::Open(GDALOpenInfo *poOpenInfo)
1091 : {
1092 82 : CPLString osCollectionDescURL;
1093 :
1094 41 : m_osRootURL = CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "URL",
1095 41 : poOpenInfo->pszFilename);
1096 41 : if (STARTS_WITH_CI(m_osRootURL, "WFS3:"))
1097 1 : m_osRootURL = m_osRootURL.substr(strlen("WFS3:"));
1098 40 : else if (STARTS_WITH_CI(m_osRootURL, "OAPIF:"))
1099 36 : m_osRootURL = m_osRootURL.substr(strlen("OAPIF:"));
1100 4 : else if (STARTS_WITH_CI(m_osRootURL, "OAPIF_COLLECTION:"))
1101 : {
1102 : // Used by the OGCAPI driver
1103 2 : osCollectionDescURL = m_osRootURL.substr(strlen("OAPIF_COLLECTION:"));
1104 2 : m_osRootURL = osCollectionDescURL;
1105 : }
1106 :
1107 41 : const auto nPosQuestionMark = m_osRootURL.find('?');
1108 41 : if (nPosQuestionMark != std::string::npos)
1109 : {
1110 3 : m_osUserQueryParams = m_osRootURL.substr(nPosQuestionMark + 1);
1111 3 : m_osRootURL.resize(nPosQuestionMark);
1112 : }
1113 :
1114 41 : const auto nCollectionsPos = m_osRootURL.find("/collections/");
1115 41 : if (nCollectionsPos != std::string::npos)
1116 : {
1117 4 : if (osCollectionDescURL.empty())
1118 2 : osCollectionDescURL = m_osRootURL;
1119 4 : m_osRootURL.resize(nCollectionsPos);
1120 : }
1121 :
1122 : // m_osServerBaseURL is just the "https://example.com" part from
1123 : // "https://example.com/foo/bar"
1124 41 : m_osServerBaseURL = m_osRootURL;
1125 : {
1126 41 : const char *pszStr = m_osServerBaseURL.c_str();
1127 41 : const char *pszPtr = pszStr;
1128 41 : if (STARTS_WITH(pszPtr, "http://"))
1129 41 : pszPtr += strlen("http://");
1130 0 : else if (STARTS_WITH(pszPtr, "https://"))
1131 0 : pszPtr += strlen("https://");
1132 41 : pszPtr = strchr(pszPtr, '/');
1133 41 : if (pszPtr)
1134 41 : m_osServerBaseURL.assign(pszStr, pszPtr - pszStr);
1135 : }
1136 :
1137 41 : m_bIgnoreSchema = CPLTestBool(CSLFetchNameValueDef(
1138 41 : poOpenInfo->papszOpenOptions, "IGNORE_SCHEMA", "FALSE"));
1139 :
1140 82 : const int pageSize = atoi(
1141 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "PAGE_SIZE", "-1"));
1142 :
1143 41 : if (pageSize > 0)
1144 : {
1145 0 : m_nPageSize = pageSize;
1146 0 : m_bPageSizeSetFromOpenOptions = true;
1147 : }
1148 :
1149 : m_osDateTime =
1150 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "DATETIME", "");
1151 :
1152 82 : const int initialRequestPageSize = atoi(CSLFetchNameValueDef(
1153 41 : poOpenInfo->papszOpenOptions, "INITIAL_REQUEST_PAGE_SIZE", "-1"));
1154 :
1155 41 : if (initialRequestPageSize >= 1)
1156 : {
1157 2 : m_nInitialRequestPageSize = initialRequestPageSize;
1158 : }
1159 :
1160 : m_osUserPwd =
1161 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "USERPWD", "");
1162 : std::string osCRS =
1163 82 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", "");
1164 : std::string osPreferredCRS =
1165 82 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "PREFERRED_CRS", "");
1166 41 : if (!osCRS.empty())
1167 : {
1168 2 : if (!osPreferredCRS.empty())
1169 : {
1170 0 : CPLError(
1171 : CE_Failure, CPLE_AppDefined,
1172 : "CRS and PREFERRED_CRS open options are mutually exclusive.");
1173 0 : return false;
1174 : }
1175 2 : m_osAskedCRS = osCRS;
1176 2 : if (m_oAskedCRS.SetFromUserInput(
1177 : osCRS.c_str(),
1178 2 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
1179 : OGRERR_NONE)
1180 : {
1181 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid value for CRS");
1182 0 : return false;
1183 : }
1184 2 : m_bAskedCRSIsRequired = true;
1185 : }
1186 39 : else if (!osPreferredCRS.empty())
1187 : {
1188 2 : m_osAskedCRS = osPreferredCRS;
1189 2 : if (m_oAskedCRS.SetFromUserInput(
1190 : osPreferredCRS.c_str(),
1191 2 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
1192 : OGRERR_NONE)
1193 : {
1194 0 : CPLError(CE_Failure, CPLE_AppDefined,
1195 : "Invalid value for PREFERRED_CRS");
1196 0 : return false;
1197 : }
1198 : }
1199 :
1200 41 : m_bServerFeaturesAxisOrderGISFriendly =
1201 41 : EQUAL(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
1202 : "SERVER_FEATURE_AXIS_ORDER",
1203 : "AUTHORITY_COMPLIANT"),
1204 : "GIS_FRIENDLY");
1205 :
1206 82 : CPLString osResult;
1207 82 : CPLString osContentType;
1208 :
1209 41 : if (!osCollectionDescURL.empty())
1210 : {
1211 4 : if (!Download(osCollectionDescURL, MEDIA_TYPE_JSON, osResult,
1212 : osContentType))
1213 : {
1214 0 : return false;
1215 : }
1216 8 : CPLJSONDocument oDoc;
1217 4 : if (!oDoc.LoadMemory(osResult))
1218 : {
1219 0 : return false;
1220 : }
1221 4 : const auto &oRoot = oDoc.GetRoot();
1222 4 : return LoadJSONCollection(oRoot, CPLJSONArray());
1223 : }
1224 :
1225 : const std::string osCollectionsURL(
1226 111 : ConcatenateURLParts(m_osRootURL, "/collections"));
1227 37 : if (!Download(osCollectionsURL, MEDIA_TYPE_JSON, osResult, osContentType))
1228 : {
1229 4 : return false;
1230 : }
1231 :
1232 33 : if (osContentType.find("json") != std::string::npos)
1233 : {
1234 33 : return LoadJSONCollections(osResult, osCollectionsURL);
1235 : }
1236 :
1237 0 : return true;
1238 : }
1239 :
1240 : /************************************************************************/
1241 : /* GetLayer() */
1242 : /************************************************************************/
1243 :
1244 36 : OGRLayer *OGROAPIFDataset::GetLayer(int nIndex)
1245 : {
1246 36 : if (nIndex < 0 || nIndex >= GetLayerCount())
1247 0 : return nullptr;
1248 36 : return m_apoLayers[nIndex].get();
1249 : }
1250 :
1251 : /************************************************************************/
1252 : /* Identify() */
1253 : /************************************************************************/
1254 :
1255 45307 : static int OGROAPIFDriverIdentify(GDALOpenInfo *poOpenInfo)
1256 :
1257 : {
1258 90612 : return STARTS_WITH_CI(poOpenInfo->pszFilename, "WFS3:") ||
1259 45305 : STARTS_WITH_CI(poOpenInfo->pszFilename, "OAPIF:") ||
1260 135841 : STARTS_WITH_CI(poOpenInfo->pszFilename, "OAPIF_COLLECTION:") ||
1261 45229 : (poOpenInfo->IsSingleAllowedDriver("OAPIF") &&
1262 4 : (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
1263 45307 : STARTS_WITH(poOpenInfo->pszFilename, "https://")));
1264 : }
1265 :
1266 : /************************************************************************/
1267 : /* HasGISFriendlyAxisOrder() */
1268 : /************************************************************************/
1269 :
1270 20 : static bool HasGISFriendlyAxisOrder(const OGRSpatialReference *poSRS)
1271 : {
1272 20 : const auto &axisMapping = poSRS->GetDataAxisToSRSAxisMapping();
1273 38 : return axisMapping.size() >= 2 && axisMapping[0] == 1 &&
1274 38 : axisMapping[1] == 2;
1275 : }
1276 :
1277 : /************************************************************************/
1278 : /* OGROAPIFLayer() */
1279 : /************************************************************************/
1280 :
1281 33 : OGROAPIFLayer::OGROAPIFLayer(OGROAPIFDataset *poDS, const CPLString &osName,
1282 : const CPLJSONArray &oBBOX,
1283 : const std::string &osBBOXCrs,
1284 : std::vector<std::string> &&oCRSList,
1285 : const std::string &osActiveCRS,
1286 : double dfCoordinateEpoch,
1287 33 : const CPLJSONArray &oLinks)
1288 33 : : m_poDS(poDS)
1289 : {
1290 33 : m_poFeatureDefn = new OGRFeatureDefn(osName);
1291 33 : m_poFeatureDefn->Reference();
1292 33 : SetDescription(osName);
1293 33 : m_oSupportedCRSList = std::move(oCRSList);
1294 :
1295 33 : OGRSpatialReference *poSRS = new OGRSpatialReference();
1296 66 : poSRS->SetFromUserInput(
1297 33 : !osActiveCRS.empty() ? osActiveCRS.c_str() : SRS_WKT_WGS84_LAT_LONG,
1298 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get());
1299 33 : poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
1300 33 : m_bIsGeographicCRS = poSRS->IsGeographic();
1301 33 : m_bCRSHasGISFriendlyOrder =
1302 33 : osActiveCRS.empty() || HasGISFriendlyAxisOrder(poSRS);
1303 33 : m_osActiveCRS = osActiveCRS;
1304 33 : if (dfCoordinateEpoch > 0)
1305 2 : poSRS->SetCoordinateEpoch(dfCoordinateEpoch);
1306 33 : m_poFeatureDefn->GetGeomFieldDefn(0)->SetSpatialRef(poSRS);
1307 :
1308 33 : poSRS->Release();
1309 :
1310 33 : if (oBBOX.IsValid() && oBBOX.Size() > 0)
1311 : {
1312 20 : CPLJSONArray oRealBBOX;
1313 : // In the final 1.0.0 spec, spatial.bbox is an array (normally with
1314 : // a single element) of 4-element arrays
1315 10 : if (oBBOX[0].GetType() == CPLJSONObject::Type::Array)
1316 : {
1317 3 : oRealBBOX = oBBOX[0].ToArray();
1318 : }
1319 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
1320 7 : else if (oBBOX.Size() == 4 || oBBOX.Size() == 6)
1321 : {
1322 7 : oRealBBOX = oBBOX;
1323 : }
1324 : #endif
1325 10 : if (oRealBBOX.Size() == 4 || oRealBBOX.Size() == 6)
1326 : {
1327 10 : m_oOriginalExtent.MinX = oRealBBOX[0].ToDouble();
1328 10 : m_oOriginalExtent.MinY = oRealBBOX[1].ToDouble();
1329 10 : m_oOriginalExtent.MaxX =
1330 10 : oRealBBOX[oRealBBOX.Size() == 6 ? 3 : 2].ToDouble();
1331 10 : m_oOriginalExtent.MaxY =
1332 10 : oRealBBOX[oRealBBOX.Size() == 6 ? 4 : 3].ToDouble();
1333 :
1334 20 : m_oOriginalExtentCRS.SetFromUserInput(
1335 10 : !osBBOXCrs.empty() ? osBBOXCrs.c_str() : OGC_CRS84_WKT,
1336 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get());
1337 :
1338 : // Handle bbox over antimeridian, which we do not support properly
1339 : // in OGR
1340 10 : if (m_oOriginalExtentCRS.IsGeographic())
1341 : {
1342 : const bool bSwitchXY =
1343 10 : !HasGISFriendlyAxisOrder(&m_oOriginalExtentCRS);
1344 10 : if (bSwitchXY)
1345 : {
1346 0 : std::swap(m_oOriginalExtent.MinX, m_oOriginalExtent.MinY);
1347 0 : std::swap(m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY);
1348 : }
1349 :
1350 10 : if (m_oOriginalExtent.MinX > m_oOriginalExtent.MaxX &&
1351 0 : fabs(m_oOriginalExtent.MinX) <= 180.0 &&
1352 0 : fabs(m_oOriginalExtent.MaxX) <= 180.0)
1353 : {
1354 0 : m_oOriginalExtent.MinX = -180.0;
1355 0 : m_oOriginalExtent.MaxX = 180.0;
1356 : }
1357 :
1358 10 : if (bSwitchXY)
1359 : {
1360 0 : std::swap(m_oOriginalExtent.MinX, m_oOriginalExtent.MinY);
1361 0 : std::swap(m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY);
1362 : }
1363 : }
1364 : }
1365 : }
1366 :
1367 : // Default to what the spec mandates for the /items URL, but check links
1368 : // later
1369 33 : m_osURL = ConcatenateURLParts(m_poDS->m_osRootURL,
1370 66 : "/collections/" + osName + "/items");
1371 66 : const std::string osParentURL(m_osURL);
1372 33 : m_osPath = "/collections/" + osName + "/items";
1373 :
1374 33 : if (oLinks.IsValid())
1375 : {
1376 76 : for (int i = 0; i < oLinks.Size(); i++)
1377 : {
1378 72 : CPLJSONObject oLink = oLinks[i];
1379 144 : if (!oLink.IsValid() ||
1380 72 : oLink.GetType() != CPLJSONObject::Type::Object)
1381 : {
1382 0 : continue;
1383 : }
1384 216 : const auto osRel(oLink.GetString("rel"));
1385 216 : const auto osURL = oLink.GetString("href");
1386 216 : const auto type = oLink.GetString("type");
1387 72 : if (EQUAL(osRel.c_str(), "describedby"))
1388 : {
1389 4 : if (type == MEDIA_TYPE_TEXT_XML ||
1390 2 : type == MEDIA_TYPE_APPLICATION_XML)
1391 : {
1392 1 : m_osDescribedByURL = osURL;
1393 1 : m_osDescribedByType = type;
1394 1 : m_bDescribedByIsXML = true;
1395 : }
1396 2 : else if (type == MEDIA_TYPE_JSON_SCHEMA &&
1397 1 : m_osDescribedByURL.empty())
1398 : {
1399 1 : m_osDescribedByURL = osURL;
1400 1 : m_osDescribedByType = type;
1401 1 : m_bDescribedByIsXML = false;
1402 : }
1403 : }
1404 70 : else if (EQUAL(osRel.c_str(), "queryables"))
1405 : {
1406 0 : if (type == MEDIA_TYPE_JSON || m_osQueryablesURL.empty())
1407 : {
1408 0 : m_osQueryablesURL = m_poDS->ResolveURL(osURL, osParentURL);
1409 : }
1410 : }
1411 70 : else if (EQUAL(osRel.c_str(), "items"))
1412 : {
1413 12 : if (type == MEDIA_TYPE_GEOJSON)
1414 : {
1415 2 : m_osURL = m_poDS->ResolveURL(osURL, osParentURL);
1416 : }
1417 : }
1418 : }
1419 4 : if (!m_osDescribedByURL.empty())
1420 : {
1421 : m_osDescribedByURL =
1422 2 : m_poDS->ResolveURL(m_osDescribedByURL, osParentURL);
1423 : }
1424 : }
1425 :
1426 33 : OGROAPIFLayer::ResetReading();
1427 33 : }
1428 :
1429 : /************************************************************************/
1430 : /* ~OGROAPIFLayer() */
1431 : /************************************************************************/
1432 :
1433 66 : OGROAPIFLayer::~OGROAPIFLayer()
1434 : {
1435 33 : m_poFeatureDefn->Release();
1436 66 : }
1437 :
1438 : /************************************************************************/
1439 : /* GetSupportedSRSList() */
1440 : /************************************************************************/
1441 :
1442 : const OGRLayer::GetSupportedSRSListRetType &
1443 5 : OGROAPIFLayer::GetSupportedSRSList(int /*iGeomField*/)
1444 : {
1445 5 : if (!m_oSupportedCRSList.empty() && m_apoSupportedCRSList.empty())
1446 : {
1447 6 : for (const auto &osCRS : m_oSupportedCRSList)
1448 : {
1449 : auto poSRS = std::unique_ptr<OGRSpatialReference,
1450 : OGRSpatialReferenceReleaser>(
1451 8 : new OGRSpatialReference());
1452 4 : if (poSRS->SetFromUserInput(
1453 : osCRS.c_str(),
1454 : OGRSpatialReference::
1455 4 : SET_FROM_USER_INPUT_LIMITATIONS_get()) == OGRERR_NONE)
1456 : {
1457 4 : m_apoSupportedCRSList.emplace_back(std::move(poSRS));
1458 : }
1459 : }
1460 : }
1461 5 : return m_apoSupportedCRSList;
1462 : }
1463 :
1464 : /************************************************************************/
1465 : /* SetActiveSRS() */
1466 : /************************************************************************/
1467 :
1468 5 : OGRErr OGROAPIFLayer::SetActiveSRS(int /*iGeomField*/,
1469 : const OGRSpatialReference *poSRS)
1470 : {
1471 5 : if (poSRS == nullptr)
1472 1 : return OGRERR_FAILURE;
1473 4 : const char *const apszOptions[] = {
1474 : "IGNORE_DATA_AXIS_TO_SRS_AXIS_MAPPING=YES", nullptr};
1475 7 : for (const auto &osCRS : m_oSupportedCRSList)
1476 : {
1477 6 : OGRSpatialReference oTmpSRS;
1478 6 : if (oTmpSRS.SetFromUserInput(
1479 : osCRS.c_str(),
1480 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
1481 12 : OGRERR_NONE &&
1482 6 : oTmpSRS.IsSame(poSRS, apszOptions))
1483 : {
1484 3 : m_osActiveCRS = osCRS;
1485 3 : auto poGeomFieldDefn = m_poFeatureDefn->GetGeomFieldDefn(0);
1486 3 : if (poGeomFieldDefn)
1487 : {
1488 3 : OGRSpatialReference *poSRSClone = poSRS->Clone();
1489 3 : poSRSClone->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
1490 3 : poGeomFieldDefn->SetSpatialRef(poSRSClone);
1491 3 : m_bIsGeographicCRS = poSRSClone->IsGeographic();
1492 3 : m_bCRSHasGISFriendlyOrder = HasGISFriendlyAxisOrder(poSRSClone);
1493 3 : poSRSClone->Release();
1494 : }
1495 3 : m_oExtent = OGREnvelope();
1496 3 : SetSpatialFilter(nullptr);
1497 3 : ResetReading();
1498 3 : return OGRERR_NONE;
1499 : }
1500 : }
1501 1 : return OGRERR_FAILURE;
1502 : }
1503 :
1504 : /************************************************************************/
1505 : /* ComputeExtent() */
1506 : /************************************************************************/
1507 :
1508 8 : void OGROAPIFLayer::ComputeExtent()
1509 : {
1510 8 : m_oExtent = m_oOriginalExtent;
1511 8 : const auto poGeomFieldDefn = m_poFeatureDefn->GetGeomFieldDefn(0);
1512 8 : if (poGeomFieldDefn)
1513 : {
1514 8 : const OGRSpatialReference *poSRS = poGeomFieldDefn->GetSpatialRef();
1515 8 : if (poSRS && !poSRS->IsSame(&m_oOriginalExtentCRS))
1516 : {
1517 : auto poCT = std::unique_ptr<OGRCoordinateTransformation>(
1518 8 : OGRCreateCoordinateTransformation(&m_oOriginalExtentCRS,
1519 16 : poSRS));
1520 8 : if (poCT)
1521 : {
1522 8 : poCT->TransformBounds(
1523 : m_oOriginalExtent.MinX, m_oOriginalExtent.MinY,
1524 : m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY,
1525 : &m_oExtent.MinX, &m_oExtent.MinY, &m_oExtent.MaxX,
1526 8 : &m_oExtent.MaxY, 20);
1527 : }
1528 : }
1529 : }
1530 8 : }
1531 :
1532 : /************************************************************************/
1533 : /* SetItemAssets() */
1534 : /************************************************************************/
1535 :
1536 1 : void OGROAPIFLayer::SetItemAssets(const CPLJSONObject &oItemAssets)
1537 : {
1538 2 : auto oChildren = oItemAssets.GetChildren();
1539 3 : for (const auto &oItemAsset : oChildren)
1540 : {
1541 2 : m_aosItemAssetNames.emplace_back(oItemAsset.GetName());
1542 : }
1543 1 : }
1544 :
1545 : /************************************************************************/
1546 : /* ResolveRefs() */
1547 : /************************************************************************/
1548 :
1549 205 : static CPLJSONObject ResolveRefs(const CPLJSONObject &oRoot,
1550 : const CPLJSONObject &oObj)
1551 : {
1552 615 : const auto osRef = oObj.GetString("$ref");
1553 205 : if (osRef.empty())
1554 179 : return oObj;
1555 26 : if (STARTS_WITH(osRef.c_str(), "#/"))
1556 : {
1557 25 : return oRoot.GetObj(osRef.c_str() + 2);
1558 : }
1559 2 : CPLJSONObject oInvalid;
1560 1 : oInvalid.Deinit();
1561 1 : return oInvalid;
1562 : }
1563 :
1564 : /************************************************************************/
1565 : /* BuildExampleRecursively() */
1566 : /************************************************************************/
1567 :
1568 205 : static bool BuildExampleRecursively(CPLJSONObject &oRes,
1569 : const CPLJSONObject &oRoot,
1570 : const CPLJSONObject &oObjIn)
1571 : {
1572 410 : auto oResolvedObj = ResolveRefs(oRoot, oObjIn);
1573 205 : if (!oResolvedObj.IsValid())
1574 1 : return false;
1575 612 : const auto osType = oResolvedObj.GetString("type");
1576 204 : if (osType == "object")
1577 : {
1578 87 : const auto oAllOf = oResolvedObj.GetArray("allOf");
1579 58 : const auto oProperties = oResolvedObj.GetObj("properties");
1580 29 : if (oAllOf.IsValid())
1581 : {
1582 13 : for (int i = 0; i < oAllOf.Size(); i++)
1583 : {
1584 20 : CPLJSONObject oChildRes;
1585 20 : if (BuildExampleRecursively(oChildRes, oRoot, oAllOf[i]) &&
1586 10 : oChildRes.GetType() == CPLJSONObject::Type::Object)
1587 : {
1588 20 : auto oChildren = oChildRes.GetChildren();
1589 89 : for (const auto &oChild : oChildren)
1590 : {
1591 79 : oRes.Add(oChild.GetName(), oChild);
1592 : }
1593 : }
1594 : }
1595 : }
1596 26 : else if (oProperties.IsValid())
1597 : {
1598 50 : auto oChildren = oProperties.GetChildren();
1599 206 : for (const auto &oChild : oChildren)
1600 : {
1601 362 : CPLJSONObject oChildRes;
1602 181 : if (BuildExampleRecursively(oChildRes, oRoot, oChild))
1603 : {
1604 180 : oRes.Add(oChild.GetName(), oChildRes);
1605 : }
1606 : else
1607 : {
1608 1 : oRes.Add(oChild.GetName(), "unknown type");
1609 : }
1610 : }
1611 : }
1612 29 : return true;
1613 : }
1614 175 : else if (osType == "array")
1615 : {
1616 26 : CPLJSONArray oArray;
1617 26 : const auto oItems = oResolvedObj.GetObj("items");
1618 13 : if (oItems.IsValid())
1619 : {
1620 26 : CPLJSONObject oChildRes;
1621 13 : if (BuildExampleRecursively(oChildRes, oRoot, oItems))
1622 : {
1623 12 : oArray.Add(oChildRes);
1624 : }
1625 : }
1626 13 : oRes = std::move(oArray);
1627 13 : return true;
1628 : }
1629 162 : else if (osType == "string")
1630 : {
1631 234 : CPLJSONObject oTemp;
1632 234 : const auto osFormat = oResolvedObj.GetString("format");
1633 117 : if (!osFormat.empty())
1634 27 : oTemp.Set("_", osFormat);
1635 : else
1636 90 : oTemp.Set("_", "string");
1637 117 : oRes = oTemp.GetObj("_");
1638 117 : return true;
1639 : }
1640 45 : else if (osType == "number")
1641 : {
1642 30 : CPLJSONObject oTemp;
1643 30 : oTemp.Set("_", 1.25);
1644 30 : oRes = oTemp.GetObj("_");
1645 30 : return true;
1646 : }
1647 15 : else if (osType == "integer")
1648 : {
1649 14 : CPLJSONObject oTemp;
1650 14 : oTemp.Set("_", 1);
1651 14 : oRes = oTemp.GetObj("_");
1652 14 : return true;
1653 : }
1654 1 : else if (osType == "boolean")
1655 : {
1656 0 : CPLJSONObject oTemp;
1657 0 : oTemp.Set("_", true);
1658 0 : oRes = oTemp.GetObj("_");
1659 0 : return true;
1660 : }
1661 1 : else if (osType == "null")
1662 : {
1663 0 : CPLJSONObject oTemp;
1664 0 : oTemp.SetNull("_");
1665 0 : oRes = oTemp.GetObj("_");
1666 0 : return true;
1667 : }
1668 :
1669 1 : return false;
1670 : }
1671 :
1672 : /************************************************************************/
1673 : /* GetObjectExampleFromSchema() */
1674 : /************************************************************************/
1675 :
1676 1 : static CPLJSONObject GetObjectExampleFromSchema(const std::string &osJSONSchema)
1677 : {
1678 2 : CPLJSONDocument oDoc;
1679 1 : if (!oDoc.LoadMemory(osJSONSchema))
1680 : {
1681 0 : CPLJSONObject oInvalid;
1682 0 : oInvalid.Deinit();
1683 0 : return oInvalid;
1684 : }
1685 2 : const auto &oRoot = oDoc.GetRoot();
1686 2 : CPLJSONObject oRes;
1687 1 : BuildExampleRecursively(oRes, oRoot, oRoot);
1688 1 : return oRes;
1689 : }
1690 :
1691 : /************************************************************************/
1692 : /* GetSchema() */
1693 : /************************************************************************/
1694 :
1695 30 : void OGROAPIFLayer::GetSchema()
1696 : {
1697 30 : if (m_osDescribedByURL.empty() || m_poDS->m_bIgnoreSchema)
1698 28 : return;
1699 :
1700 4 : CPLErrorStateBackuper oErrorStateBackuper(CPLQuietErrorHandler);
1701 :
1702 2 : if (m_bDescribedByIsXML)
1703 : {
1704 2 : std::vector<GMLFeatureClass *> apoClasses;
1705 1 : bool bFullyUnderstood = false;
1706 1 : bool bUseSchemaImports = false;
1707 1 : bool bHaveSchema = GMLParseXSD(m_osDescribedByURL, bUseSchemaImports,
1708 : apoClasses, bFullyUnderstood);
1709 1 : if (bHaveSchema && apoClasses.size() == 1)
1710 : {
1711 1 : CPLDebug("OAPIF", "Using XML schema");
1712 1 : auto poGMLFeatureClass = apoClasses[0];
1713 1 : if (poGMLFeatureClass->GetGeometryPropertyCount() == 1)
1714 : {
1715 : // Force linear type as we work with GeoJSON data
1716 1 : m_poFeatureDefn->SetGeomType(
1717 : OGR_GT_GetLinear(static_cast<OGRwkbGeometryType>(
1718 1 : poGMLFeatureClass->GetGeometryProperty(0)->GetType())));
1719 : }
1720 :
1721 1 : const int nPropertyCount = poGMLFeatureClass->GetPropertyCount();
1722 : // This is a hack for
1723 : // http://www.pvretano.com/cubewerx/cubeserv/default/wfs/3.0.0/framework/collections/UNINCORPORATED_PL/schema
1724 : // The GML representation has attributes starting all with
1725 : // "UNINCORPORATED_PL." whereas the GeoJSON output not
1726 2 : CPLString osPropertyNamePrefix(GetName());
1727 1 : osPropertyNamePrefix += '.';
1728 1 : bool bAllPrefixed = true;
1729 2 : for (int iField = 0; iField < nPropertyCount; iField++)
1730 : {
1731 1 : const auto poProperty = poGMLFeatureClass->GetProperty(iField);
1732 1 : if (!STARTS_WITH(poProperty->GetName(),
1733 : osPropertyNamePrefix.c_str()))
1734 : {
1735 1 : bAllPrefixed = false;
1736 : }
1737 : }
1738 2 : for (int iField = 0; iField < nPropertyCount; iField++)
1739 : {
1740 1 : const auto poProperty = poGMLFeatureClass->GetProperty(iField);
1741 1 : OGRFieldSubType eSubType = OFSTNone;
1742 : const OGRFieldType eFType =
1743 1 : GML_GetOGRFieldType(poProperty->GetType(), eSubType);
1744 :
1745 : const char *pszName =
1746 1 : poProperty->GetName() +
1747 0 : (bAllPrefixed ? osPropertyNamePrefix.size() : 0);
1748 2 : auto poField = std::make_unique<OGRFieldDefn>(pszName, eFType);
1749 1 : poField->SetSubType(eSubType);
1750 1 : m_apoFieldsFromSchema.emplace_back(std::move(poField));
1751 : }
1752 : }
1753 :
1754 2 : for (auto poFeatureClass : apoClasses)
1755 1 : delete poFeatureClass;
1756 : }
1757 : else
1758 : {
1759 2 : CPLString osContentType;
1760 2 : CPLString osResult;
1761 1 : if (!m_poDS->Download(m_osDescribedByURL, m_osDescribedByType, osResult,
1762 : osContentType))
1763 : {
1764 0 : CPLDebug("OAPIF", "Could not download schema");
1765 : }
1766 : else
1767 : {
1768 2 : const auto oExample = GetObjectExampleFromSchema(osResult);
1769 : // CPLDebug("OAPIF", "Example from schema: %s",
1770 : // oExample.Format(CPLJSONObject::PrettyFormat::Pretty).c_str());
1771 2 : if (oExample.IsValid() &&
1772 1 : oExample.GetType() == CPLJSONObject::Type::Object)
1773 : {
1774 3 : const auto oProperties = oExample.GetObj("properties");
1775 2 : if (oProperties.IsValid() &&
1776 1 : oProperties.GetType() == CPLJSONObject::Type::Object)
1777 : {
1778 1 : CPLDebug("OAPIF", "Using JSON schema");
1779 2 : const auto oProps = oProperties.GetChildren();
1780 19 : for (const auto &oProp : oProps)
1781 : {
1782 18 : OGRFieldType eType = OFTString;
1783 18 : OGRFieldSubType eSubType = OFSTNone;
1784 18 : const auto oType = oProp.GetType();
1785 18 : if (oType == CPLJSONObject::Type::String)
1786 : {
1787 13 : if (oProp.ToString() == "date-time")
1788 : {
1789 4 : eType = OFTDateTime;
1790 : }
1791 9 : else if (oProp.ToString() == "date")
1792 : {
1793 0 : eType = OFTDate;
1794 : }
1795 : }
1796 5 : else if (oType == CPLJSONObject::Type::Boolean)
1797 : {
1798 0 : eType = OFTInteger;
1799 0 : eSubType = OFSTBoolean;
1800 : }
1801 5 : else if (oType == CPLJSONObject::Type::Double)
1802 : {
1803 0 : eType = OFTReal;
1804 : }
1805 5 : else if (oType == CPLJSONObject::Type::Integer)
1806 : {
1807 0 : eType = OFTInteger;
1808 : }
1809 5 : else if (oType == CPLJSONObject::Type::Long)
1810 : {
1811 0 : eType = OFTInteger64;
1812 : }
1813 5 : else if (oType == CPLJSONObject::Type::Array)
1814 : {
1815 4 : const auto oArray = oProp.ToArray();
1816 2 : if (oArray.Size() > 0)
1817 : {
1818 1 : if (oArray[0].GetType() ==
1819 : CPLJSONObject::Type::String)
1820 0 : eType = OFTStringList;
1821 1 : else if (oArray[0].GetType() ==
1822 : CPLJSONObject::Type::Integer)
1823 0 : eType = OFTIntegerList;
1824 : }
1825 : }
1826 :
1827 : auto poField = std::make_unique<OGRFieldDefn>(
1828 36 : oProp.GetName().c_str(), eType);
1829 18 : poField->SetSubType(eSubType);
1830 18 : m_apoFieldsFromSchema.emplace_back(std::move(poField));
1831 : }
1832 : }
1833 : }
1834 : }
1835 : }
1836 : }
1837 :
1838 : /************************************************************************/
1839 : /* GetLayerDefn() */
1840 : /************************************************************************/
1841 :
1842 114 : OGRFeatureDefn *OGROAPIFLayer::GetLayerDefn()
1843 : {
1844 114 : if (!m_bFeatureDefnEstablished)
1845 25 : EstablishFeatureDefn();
1846 114 : return m_poFeatureDefn;
1847 : }
1848 :
1849 : /************************************************************************/
1850 : /* EstablishFeatureDefn() */
1851 : /************************************************************************/
1852 :
1853 30 : void OGROAPIFLayer::EstablishFeatureDefn()
1854 : {
1855 30 : CPLAssert(!m_bFeatureDefnEstablished);
1856 30 : m_bFeatureDefnEstablished = true;
1857 :
1858 30 : GetSchema();
1859 :
1860 30 : if (!m_poDS->m_bPageSizeSetFromOpenOptions)
1861 : {
1862 30 : const int nOldPageSize{m_poDS->m_nPageSize};
1863 30 : m_poDS->DeterminePageSizeFromAPI(m_osURL);
1864 : // cppcheck-suppress knownConditionTrueFalse
1865 30 : if (nOldPageSize != m_poDS->m_nPageSize)
1866 : {
1867 4 : m_osGetURL = CPLURLAddKVP(m_osGetURL, "limit",
1868 4 : CPLSPrintf("%d", m_poDS->m_nPageSize));
1869 : }
1870 : }
1871 :
1872 30 : CPLJSONDocument oDoc;
1873 30 : CPLString osURL(m_osURL);
1874 :
1875 60 : osURL = CPLURLAddKVP(
1876 : osURL, "limit",
1877 30 : CPLSPrintf("%d", std::min(m_poDS->m_nInitialRequestPageSize,
1878 60 : m_poDS->m_nPageSize)));
1879 30 : if (!m_poDS->DownloadJSon(osURL, oDoc))
1880 0 : return;
1881 :
1882 30 : const CPLString osTmpFilename(VSIMemGenerateHiddenFilename("oapif.json"));
1883 30 : oDoc.Save(osTmpFilename);
1884 : std::unique_ptr<GDALDataset> poDS(GDALDataset::FromHandle(
1885 : GDALOpenEx(osTmpFilename, GDAL_OF_VECTOR | GDAL_OF_INTERNAL, nullptr,
1886 30 : nullptr, nullptr)));
1887 30 : VSIUnlink(osTmpFilename);
1888 30 : if (!poDS.get())
1889 1 : return;
1890 29 : OGRLayer *poLayer = poDS->GetLayer(0);
1891 29 : if (!poLayer)
1892 0 : return;
1893 29 : OGRFeatureDefn *poFeatureDefn = poLayer->GetLayerDefn();
1894 29 : if (m_poFeatureDefn->GetGeomType() == wkbUnknown)
1895 : {
1896 28 : m_poFeatureDefn->SetGeomType(poFeatureDefn->GetGeomType());
1897 : }
1898 29 : if (m_apoFieldsFromSchema.empty())
1899 : {
1900 85 : for (int i = 0; i < poFeatureDefn->GetFieldCount(); i++)
1901 : {
1902 58 : m_poFeatureDefn->AddFieldDefn(poFeatureDefn->GetFieldDefn(i));
1903 : }
1904 : }
1905 : else
1906 : {
1907 3 : if (poFeatureDefn->GetFieldCount() > 0 &&
1908 1 : strcmp(poFeatureDefn->GetFieldDefn(0)->GetNameRef(), "id") == 0)
1909 : {
1910 0 : m_poFeatureDefn->AddFieldDefn(poFeatureDefn->GetFieldDefn(0));
1911 : }
1912 21 : for (const auto &poField : m_apoFieldsFromSchema)
1913 : {
1914 19 : m_poFeatureDefn->AddFieldDefn(poField.get());
1915 : }
1916 : // In case there would be properties found in sample, but not in
1917 : // schema...
1918 3 : for (int i = 0; i < poFeatureDefn->GetFieldCount(); i++)
1919 : {
1920 1 : auto poFDefn = poFeatureDefn->GetFieldDefn(i);
1921 1 : if (m_poFeatureDefn->GetFieldIndex(poFDefn->GetNameRef()) < 0)
1922 : {
1923 1 : m_poFeatureDefn->AddFieldDefn(poFDefn);
1924 : }
1925 : }
1926 : }
1927 :
1928 31 : for (const auto &osItemAsset : m_aosItemAssetNames)
1929 : {
1930 4 : OGRFieldDefn oFieldDefn(("asset_" + osItemAsset + "_href").c_str(),
1931 4 : OFTString);
1932 : // cppcheck-suppress danglingTemporaryLifetime
1933 2 : m_poFeatureDefn->AddFieldDefn(&oFieldDefn);
1934 : }
1935 :
1936 58 : const auto &oRoot = oDoc.GetRoot();
1937 29 : GIntBig nFeatures = oRoot.GetLong("numberMatched", -1);
1938 29 : if (nFeatures >= 0)
1939 : {
1940 9 : m_nTotalFeatureCount = nFeatures;
1941 : }
1942 :
1943 87 : auto oFeatures = oRoot.GetArray("features");
1944 29 : if (oFeatures.IsValid() && oFeatures.Size() > 0)
1945 : {
1946 25 : auto eType = oFeatures[0].GetObj("id").GetType();
1947 25 : if (eType == CPLJSONObject::Type::Integer ||
1948 : eType == CPLJSONObject::Type::Long)
1949 : {
1950 3 : m_bHasIntIdMember = true;
1951 : }
1952 22 : else if (eType == CPLJSONObject::Type::String)
1953 : {
1954 7 : m_bHasStringIdMember = true;
1955 : }
1956 : }
1957 : }
1958 :
1959 : /************************************************************************/
1960 : /* ResetReading() */
1961 : /************************************************************************/
1962 :
1963 79 : void OGROAPIFLayer::ResetReading()
1964 : {
1965 79 : m_poUnderlyingDS.reset();
1966 79 : m_poUnderlyingLayer = nullptr;
1967 79 : m_nFID = 1;
1968 79 : m_osGetURL = m_osURL;
1969 79 : if (!m_osGetID.empty())
1970 : {
1971 4 : m_osGetURL += "/" + m_osGetID;
1972 : }
1973 : else
1974 : {
1975 75 : if (m_poDS->m_nPageSize > 0)
1976 : {
1977 150 : m_osGetURL = CPLURLAddKVP(m_osGetURL, "limit",
1978 150 : CPLSPrintf("%d", m_poDS->m_nPageSize));
1979 : }
1980 75 : m_osGetURL = AddFilters(m_osGetURL);
1981 : }
1982 79 : m_oCurDoc = CPLJSONDocument();
1983 79 : m_iFeatureInPage = 0;
1984 79 : }
1985 :
1986 : /************************************************************************/
1987 : /* AddFilters() */
1988 : /************************************************************************/
1989 :
1990 77 : CPLString OGROAPIFLayer::AddFilters(const CPLString &osURL)
1991 : {
1992 77 : CPLString osURLNew(osURL);
1993 77 : if (m_poFilterGeom)
1994 : {
1995 8 : double dfMinX = m_sFilterEnvelope.MinX;
1996 8 : double dfMinY = m_sFilterEnvelope.MinY;
1997 8 : double dfMaxX = m_sFilterEnvelope.MaxX;
1998 8 : double dfMaxY = m_sFilterEnvelope.MaxY;
1999 8 : bool bAddBBoxFilter = true;
2000 8 : if (m_bIsGeographicCRS)
2001 : {
2002 6 : dfMinX = std::max(dfMinX, -180.0);
2003 6 : dfMinY = std::max(dfMinY, -90.0);
2004 6 : dfMaxX = std::min(dfMaxX, 180.0);
2005 6 : dfMaxY = std::min(dfMaxY, 90.0);
2006 8 : bAddBBoxFilter = dfMinX > -180.0 || dfMinY > -90.0 ||
2007 8 : dfMaxX < 180.0 || dfMaxY < 90.0;
2008 : }
2009 8 : if (bAddBBoxFilter)
2010 : {
2011 7 : if (!m_bCRSHasGISFriendlyOrder)
2012 : {
2013 2 : std::swap(dfMinX, dfMinY);
2014 2 : std::swap(dfMaxX, dfMaxY);
2015 : }
2016 14 : osURLNew = CPLURLAddKVP(osURLNew, "bbox",
2017 : CPLSPrintf("%.17g,%.17g,%.17g,%.17g",
2018 7 : dfMinX, dfMinY, dfMaxX, dfMaxY));
2019 7 : if (!m_osActiveCRS.empty())
2020 : {
2021 : osURLNew =
2022 4 : CPLURLAddKVP(osURLNew, "bbox-crs", m_osActiveCRS.c_str());
2023 : }
2024 : }
2025 : }
2026 77 : if (!m_osActiveCRS.empty())
2027 : {
2028 17 : osURLNew = CPLURLAddKVP(osURLNew, "crs", m_osActiveCRS.c_str());
2029 : }
2030 77 : if (!m_osAttributeFilter.empty())
2031 : {
2032 15 : if (osURLNew.find('?') == std::string::npos)
2033 0 : osURLNew += "?";
2034 : else
2035 15 : osURLNew += "&";
2036 15 : osURLNew += m_osAttributeFilter;
2037 : }
2038 77 : if (!m_poDS->m_osDateTime.empty())
2039 : {
2040 3 : if (osURLNew.find('?') == std::string::npos)
2041 0 : osURLNew += "?";
2042 : else
2043 3 : osURLNew += "&";
2044 3 : osURLNew += "datetime=";
2045 3 : osURLNew += m_poDS->m_osDateTime;
2046 : }
2047 77 : return osURLNew;
2048 : }
2049 :
2050 : /************************************************************************/
2051 : /* GetNextRawFeature() */
2052 : /************************************************************************/
2053 :
2054 45 : OGRFeature *OGROAPIFLayer::GetNextRawFeature()
2055 : {
2056 45 : if (!m_bFeatureDefnEstablished)
2057 4 : EstablishFeatureDefn();
2058 :
2059 45 : OGRFeature *poSrcFeature = nullptr;
2060 : while (true)
2061 : {
2062 52 : if (m_poUnderlyingLayer == nullptr)
2063 : {
2064 41 : if (m_osGetURL.empty())
2065 3 : return nullptr;
2066 :
2067 38 : m_oCurDoc = CPLJSONDocument();
2068 :
2069 38 : const CPLString osURL(m_osGetURL);
2070 38 : m_osGetURL.clear();
2071 38 : CPLStringList aosHeaders;
2072 38 : if (!m_poDS->DownloadJSon(osURL, m_oCurDoc,
2073 : MEDIA_TYPE_GEOJSON ", " MEDIA_TYPE_JSON,
2074 : &aosHeaders))
2075 : {
2076 0 : return nullptr;
2077 : }
2078 :
2079 : const std::string osContentCRS =
2080 38 : aosHeaders.FetchNameValueDef("Content-Crs", "");
2081 38 : if (!m_bHasEmittedContentCRSWarning && !osContentCRS.empty())
2082 : {
2083 7 : if (m_osActiveCRS.empty())
2084 : {
2085 0 : if (osContentCRS !=
2086 0 : "<http://www.opengis.net/def/crs/OGC/1.3/CRS84>" &&
2087 0 : osContentCRS !=
2088 : "<http://www.opengis.net/def/crs/OGC/0/CRS84h>")
2089 : {
2090 0 : m_bHasEmittedContentCRSWarning = true;
2091 0 : CPLDebug("OAPIF",
2092 : "Got Content-CRS = %s, but expected OGC:CRS84 "
2093 : "instead. "
2094 : "Content-CRS will be ignored",
2095 : osContentCRS.c_str());
2096 : }
2097 : }
2098 : else
2099 : {
2100 7 : if (osContentCRS != '<' + m_osActiveCRS + '>')
2101 : {
2102 0 : m_bHasEmittedContentCRSWarning = true;
2103 0 : CPLDebug(
2104 : "OAPIF",
2105 : "Got Content-CRS = %s, but expected %s instead. "
2106 : "Content-CRS will be ignored",
2107 : osContentCRS.c_str(), m_osActiveCRS.c_str());
2108 : }
2109 : }
2110 : }
2111 31 : else if (!m_bHasEmittedContentCRSWarning)
2112 : {
2113 31 : if (!m_osActiveCRS.empty())
2114 : {
2115 1 : m_bHasEmittedContentCRSWarning = true;
2116 1 : CPLDebug("OAPIF",
2117 : "Dit not get Content-CRS header. "
2118 : "Assuming %s is returned",
2119 : m_osActiveCRS.c_str());
2120 : }
2121 : }
2122 :
2123 38 : if (!m_bHasEmittedJsonCRWarning)
2124 : {
2125 114 : const auto oJsonCRS = m_oCurDoc.GetRoot().GetObj("crs");
2126 38 : if (oJsonCRS.IsValid())
2127 : {
2128 0 : m_bHasEmittedJsonCRWarning = true;
2129 0 : CPLDebug("OAPIF",
2130 : "JSON response contains %s. It will be ignored.",
2131 0 : oJsonCRS.ToString().c_str());
2132 : }
2133 : }
2134 :
2135 : const CPLString osTmpFilename(
2136 38 : VSIMemGenerateHiddenFilename("oapif.json"));
2137 38 : m_oCurDoc.Save(osTmpFilename);
2138 : m_poUnderlyingDS =
2139 76 : std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(
2140 : GDALOpenEx(osTmpFilename, GDAL_OF_VECTOR | GDAL_OF_INTERNAL,
2141 38 : nullptr, nullptr, nullptr)));
2142 38 : VSIUnlink(osTmpFilename);
2143 38 : if (!m_poUnderlyingDS.get())
2144 : {
2145 0 : return nullptr;
2146 : }
2147 38 : m_poUnderlyingLayer = m_poUnderlyingDS->GetLayer(0);
2148 38 : if (!m_poUnderlyingLayer)
2149 : {
2150 0 : m_poUnderlyingDS.reset();
2151 0 : return nullptr;
2152 : }
2153 :
2154 : // To avoid issues with implementations having a non-relevant
2155 : // next link, make sure the current page is not empty
2156 : // We could even check that the feature count is the page size
2157 : // actually
2158 38 : if (m_poUnderlyingLayer->GetFeatureCount() > 0 && m_osGetID.empty())
2159 : {
2160 108 : CPLJSONArray oLinks = m_oCurDoc.GetRoot().GetArray("links");
2161 36 : if (oLinks.IsValid())
2162 : {
2163 6 : int nCountRelNext = 0;
2164 12 : std::string osNextURL;
2165 12 : for (int i = 0; i < oLinks.Size(); i++)
2166 : {
2167 10 : CPLJSONObject oLink = oLinks[i];
2168 20 : if (!oLink.IsValid() ||
2169 10 : oLink.GetType() != CPLJSONObject::Type::Object)
2170 : {
2171 0 : continue;
2172 : }
2173 10 : if (EQUAL(oLink.GetString("rel").c_str(), "next"))
2174 : {
2175 4 : nCountRelNext++;
2176 8 : auto type = oLink.GetString("type");
2177 4 : if (type == MEDIA_TYPE_GEOJSON ||
2178 0 : type == MEDIA_TYPE_JSON)
2179 : {
2180 4 : m_osGetURL = oLink.GetString("href");
2181 4 : break;
2182 : }
2183 0 : else if (type.empty())
2184 : {
2185 0 : osNextURL = oLink.GetString("href");
2186 : }
2187 : }
2188 : }
2189 6 : if (nCountRelNext == 1 && m_osGetURL.empty())
2190 : {
2191 : // In case we go a "rel": "next" without a "type"
2192 0 : m_osGetURL = std::move(osNextURL);
2193 : }
2194 : }
2195 :
2196 : #ifdef no_longer_used
2197 : // Recommendation /rec/core/link-header
2198 : if (m_osGetURL.empty())
2199 : {
2200 : for (int i = 0; i < aosHeaders.size(); i++)
2201 : {
2202 : CPLDebug("OAPIF", "%s", aosHeaders[i]);
2203 : if (STARTS_WITH_CI(aosHeaders[i], "Link=") &&
2204 : strstr(aosHeaders[i], "rel=\"next\"") &&
2205 : strstr(aosHeaders[i],
2206 : "type=\"" MEDIA_TYPE_GEOJSON "\""))
2207 : {
2208 : const char *pszStart = strchr(aosHeaders[i], '<');
2209 : if (pszStart)
2210 : {
2211 : const char *pszEnd = strchr(pszStart + 1, '>');
2212 : if (pszEnd)
2213 : {
2214 : m_osGetURL = pszStart + 1;
2215 : m_osGetURL.resize(pszEnd - pszStart - 1);
2216 : }
2217 : }
2218 : break;
2219 : }
2220 : }
2221 : }
2222 : #endif
2223 :
2224 36 : if (!m_osGetURL.empty())
2225 : {
2226 4 : m_osGetURL = m_poDS->ResolveURL(m_osGetURL, osURL);
2227 : }
2228 : }
2229 : }
2230 :
2231 : // cppcheck-suppress nullPointerRedundantCheck
2232 49 : poSrcFeature = m_poUnderlyingLayer->GetNextFeature();
2233 49 : if (poSrcFeature)
2234 : {
2235 42 : break;
2236 : }
2237 7 : m_poUnderlyingDS.reset();
2238 7 : m_poUnderlyingLayer = nullptr;
2239 7 : m_iFeatureInPage = 0;
2240 7 : }
2241 :
2242 42 : OGRFeature *poFeature = new OGRFeature(m_poFeatureDefn);
2243 42 : poFeature->SetFrom(poSrcFeature);
2244 :
2245 : // Collect STAC assets href
2246 2 : if (!m_aosItemAssetNames.empty() && m_poUnderlyingLayer != nullptr &&
2247 44 : m_oCurDoc.GetRoot().GetArray("features").Size() ==
2248 46 : m_poUnderlyingLayer->GetFeatureCount() &&
2249 44 : m_iFeatureInPage < m_oCurDoc.GetRoot().GetArray("features").Size())
2250 : {
2251 : auto m_oFeature =
2252 6 : m_oCurDoc.GetRoot().GetArray("features")[m_iFeatureInPage];
2253 6 : auto oAssets = m_oFeature["assets"];
2254 6 : for (const auto &osAssetName : m_aosItemAssetNames)
2255 : {
2256 12 : auto href = oAssets[osAssetName]["href"];
2257 4 : if (href.IsValid() && href.GetType() == CPLJSONObject::Type::String)
2258 : {
2259 2 : poFeature->SetField(("asset_" + osAssetName + "_href").c_str(),
2260 4 : href.ToString().c_str());
2261 : }
2262 : }
2263 : }
2264 42 : m_iFeatureInPage++;
2265 :
2266 42 : auto poGeom = poFeature->GetGeometryRef();
2267 42 : if (poGeom)
2268 : {
2269 20 : if (!m_bCRSHasGISFriendlyOrder &&
2270 3 : !m_poDS->m_bServerFeaturesAxisOrderGISFriendly)
2271 2 : poGeom->swapXY();
2272 20 : poGeom->assignSpatialReference(GetSpatialRef());
2273 : }
2274 42 : if (m_bHasIntIdMember)
2275 : {
2276 4 : poFeature->SetFID(poSrcFeature->GetFID());
2277 : }
2278 : else
2279 : {
2280 38 : poFeature->SetFID(m_nFID);
2281 38 : m_nFID++;
2282 : }
2283 42 : delete poSrcFeature;
2284 42 : return poFeature;
2285 : }
2286 :
2287 : /************************************************************************/
2288 : /* GetFeature() */
2289 : /************************************************************************/
2290 :
2291 1 : OGRFeature *OGROAPIFLayer::GetFeature(GIntBig nFID)
2292 : {
2293 1 : if (!m_bFeatureDefnEstablished)
2294 0 : EstablishFeatureDefn();
2295 1 : if (!m_bHasIntIdMember)
2296 0 : return OGRLayer::GetFeature(nFID);
2297 :
2298 1 : m_osGetID.Printf(CPL_FRMT_GIB, nFID);
2299 1 : ResetReading();
2300 1 : auto poRet = GetNextRawFeature();
2301 1 : m_osGetID.clear();
2302 1 : ResetReading();
2303 1 : return poRet;
2304 : }
2305 :
2306 : /************************************************************************/
2307 : /* GetNextFeature() */
2308 : /************************************************************************/
2309 :
2310 44 : OGRFeature *OGROAPIFLayer::GetNextFeature()
2311 : {
2312 : while (true)
2313 : {
2314 44 : OGRFeature *poFeature = GetNextRawFeature();
2315 44 : if (poFeature == nullptr)
2316 3 : return nullptr;
2317 :
2318 88 : if ((m_poFilterGeom == nullptr ||
2319 82 : FilterGeometry(poFeature->GetGeometryRef())) &&
2320 41 : (m_poAttrQuery == nullptr || !m_bFilterMustBeClientSideEvaluated ||
2321 4 : m_poAttrQuery->Evaluate(poFeature)))
2322 : {
2323 40 : return poFeature;
2324 : }
2325 : else
2326 : {
2327 1 : delete poFeature;
2328 : }
2329 1 : }
2330 : }
2331 :
2332 : /************************************************************************/
2333 : /* SupportsResultTypeHits() */
2334 : /************************************************************************/
2335 :
2336 4 : bool OGROAPIFLayer::SupportsResultTypeHits()
2337 : {
2338 8 : std::string osAPIURL;
2339 8 : CPLJSONDocument oDoc = m_poDS->GetAPIDoc(osAPIURL);
2340 4 : if (oDoc.GetRoot().GetString("openapi").empty())
2341 2 : return false;
2342 :
2343 : CPLJSONArray oParameters =
2344 4 : oDoc.GetRoot().GetObj("paths").GetObj(m_osPath).GetObj("get").GetArray(
2345 6 : "parameters");
2346 2 : if (!oParameters.IsValid())
2347 0 : return false;
2348 2 : for (int i = 0; i < oParameters.Size(); i++)
2349 : {
2350 2 : CPLJSONObject oParam = oParameters[i];
2351 4 : CPLString osRef = oParam.GetString("$ref");
2352 2 : if (!osRef.empty() && osRef.find("#/") == 0)
2353 : {
2354 2 : oParam = oDoc.GetRoot().GetObj(osRef.substr(2));
2355 : #ifndef REMOVE_HACK
2356 : // Needed for
2357 : // http://www.pvretano.com/cubewerx/cubeserv/default/wfs/3.0.0/foundation/api
2358 : // that doesn't define #/components/parameters/resultType
2359 2 : if (osRef == "#/components/parameters/resultType")
2360 2 : return true;
2361 : #endif
2362 : }
2363 0 : if (oParam.GetString("name") == "resultType" &&
2364 0 : oParam.GetString("in") == "query")
2365 : {
2366 0 : CPLJSONArray oEnum = oParam.GetArray("schema/enum");
2367 0 : for (int j = 0; j < oEnum.Size(); j++)
2368 : {
2369 0 : if (oEnum[j].ToString() == "hits")
2370 0 : return true;
2371 : }
2372 0 : return false;
2373 : }
2374 : }
2375 :
2376 0 : return false;
2377 : }
2378 :
2379 : /************************************************************************/
2380 : /* GetFeatureCount() */
2381 : /************************************************************************/
2382 :
2383 10 : GIntBig OGROAPIFLayer::GetFeatureCount(int bForce)
2384 : {
2385 :
2386 20 : if (m_poFilterGeom == nullptr && m_poAttrQuery == nullptr &&
2387 10 : m_poDS->m_osDateTime.empty())
2388 : {
2389 9 : GetLayerDefn();
2390 9 : if (m_nTotalFeatureCount >= 0)
2391 : {
2392 6 : return m_nTotalFeatureCount;
2393 : }
2394 : }
2395 :
2396 4 : if (SupportsResultTypeHits() && !m_bFilterMustBeClientSideEvaluated)
2397 : {
2398 2 : CPLString osURL(m_osURL);
2399 2 : osURL = CPLURLAddKVP(osURL, "resultType", "hits");
2400 2 : osURL = AddFilters(osURL);
2401 : #ifndef REMOVE_HACK
2402 2 : bool bGMLRequest = m_osURL.find("cubeserv") != std::string::npos;
2403 : #else
2404 : constexpr bool bGMLRequest = false;
2405 : #endif
2406 2 : if (bGMLRequest)
2407 : {
2408 0 : CPLString osResult;
2409 0 : CPLString osContentType;
2410 0 : if (m_poDS->Download(osURL, MEDIA_TYPE_TEXT_XML, osResult,
2411 : osContentType))
2412 : {
2413 0 : CPLXMLNode *psDoc = CPLParseXMLString(osResult);
2414 0 : if (psDoc)
2415 : {
2416 0 : CPLXMLTreeCloser oCloser(psDoc);
2417 0 : CPL_IGNORE_RET_VAL(oCloser);
2418 0 : CPLStripXMLNamespace(psDoc, nullptr, true);
2419 : CPLString osNumberMatched = CPLGetXMLValue(
2420 0 : psDoc, "=FeatureCollection.numberMatched", "");
2421 0 : if (!osNumberMatched.empty())
2422 0 : return CPLAtoGIntBig(osNumberMatched);
2423 : }
2424 : }
2425 : }
2426 : else
2427 : {
2428 2 : CPLJSONDocument oDoc;
2429 2 : if (m_poDS->DownloadJSon(osURL, oDoc))
2430 : {
2431 2 : GIntBig nFeatures = oDoc.GetRoot().GetLong("numberMatched", -1);
2432 2 : if (nFeatures >= 0)
2433 2 : return nFeatures;
2434 : }
2435 : }
2436 : }
2437 :
2438 2 : return OGRLayer::GetFeatureCount(bForce);
2439 : }
2440 :
2441 : /************************************************************************/
2442 : /* GetExtent() */
2443 : /************************************************************************/
2444 :
2445 10 : OGRErr OGROAPIFLayer::GetExtent(OGREnvelope *psEnvelope, int bForce)
2446 : {
2447 10 : if (m_oOriginalExtent.IsInit())
2448 : {
2449 10 : if (!m_oExtent.IsInit())
2450 8 : ComputeExtent();
2451 10 : *psEnvelope = m_oExtent;
2452 10 : return OGRERR_NONE;
2453 : }
2454 0 : return OGRLayer::GetExtent(psEnvelope, bForce);
2455 : }
2456 :
2457 : /************************************************************************/
2458 : /* SetSpatialFilter() */
2459 : /************************************************************************/
2460 :
2461 10 : void OGROAPIFLayer::SetSpatialFilter(OGRGeometry *poGeomIn)
2462 : {
2463 10 : InstallFilter(poGeomIn);
2464 :
2465 10 : ResetReading();
2466 10 : }
2467 :
2468 : /************************************************************************/
2469 : /* OGRWF3ParseDateTime() */
2470 : /************************************************************************/
2471 :
2472 5 : static int OGRWF3ParseDateTime(const char *pszValue, int &nYear, int &nMonth,
2473 : int &nDay, int &nHour, int &nMinute,
2474 : int &nSecond)
2475 : {
2476 5 : int ret = sscanf(pszValue, "%04d/%02d/%02d %02d:%02d:%02d", &nYear, &nMonth,
2477 : &nDay, &nHour, &nMinute, &nSecond);
2478 5 : if (ret >= 3)
2479 0 : return ret;
2480 5 : return sscanf(pszValue, "%04d-%02d-%02dT%02d:%02d:%02d", &nYear, &nMonth,
2481 5 : &nDay, &nHour, &nMinute, &nSecond);
2482 : }
2483 :
2484 : /************************************************************************/
2485 : /* SerializeDateTime() */
2486 : /************************************************************************/
2487 :
2488 5 : static CPLString SerializeDateTime(int nDateComponents, int nYear, int nMonth,
2489 : int nDay, int nHour, int nMinute,
2490 : int nSecond)
2491 : {
2492 5 : CPLString osRet;
2493 5 : osRet.Printf("%04d-%02d-%02dT", nYear, nMonth, nDay);
2494 5 : if (nDateComponents >= 4)
2495 : {
2496 3 : osRet += CPLSPrintf("%02d", nHour);
2497 3 : if (nDateComponents >= 5)
2498 3 : osRet += CPLSPrintf(":%02d", nMinute);
2499 3 : if (nDateComponents >= 6)
2500 3 : osRet += CPLSPrintf(":%02d", nSecond);
2501 3 : osRet += "Z";
2502 : }
2503 5 : return osRet;
2504 : }
2505 :
2506 : /************************************************************************/
2507 : /* BuildFilter() */
2508 : /************************************************************************/
2509 :
2510 11 : CPLString OGROAPIFLayer::BuildFilter(const swq_expr_node *poNode)
2511 : {
2512 11 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2513 3 : poNode->nSubExprCount == 2)
2514 : {
2515 3 : const auto leftExpr = poNode->papoSubExpr[0];
2516 3 : const auto rightExpr = poNode->papoSubExpr[1];
2517 :
2518 : // Detect expression: datetime >=|> XXX and datetime <=|< XXXX
2519 3 : if (leftExpr->eNodeType == SNT_OPERATION &&
2520 3 : (leftExpr->nOperation == SWQ_GT ||
2521 3 : leftExpr->nOperation == SWQ_GE) &&
2522 1 : leftExpr->nSubExprCount == 2 &&
2523 1 : leftExpr->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2524 1 : leftExpr->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2525 1 : rightExpr->eNodeType == SNT_OPERATION &&
2526 1 : (rightExpr->nOperation == SWQ_LT ||
2527 1 : rightExpr->nOperation == SWQ_LE) &&
2528 1 : rightExpr->nSubExprCount == 2 &&
2529 1 : rightExpr->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2530 1 : rightExpr->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2531 1 : leftExpr->papoSubExpr[0]->field_index ==
2532 1 : rightExpr->papoSubExpr[0]->field_index &&
2533 1 : leftExpr->papoSubExpr[1]->field_type == SWQ_TIMESTAMP &&
2534 1 : rightExpr->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2535 : {
2536 1 : const OGRFieldDefn *poFieldDefn = GetLayerDefn()->GetFieldDefn(
2537 1 : leftExpr->papoSubExpr[0]->field_index);
2538 2 : if (poFieldDefn && (poFieldDefn->GetType() == OFTDate ||
2539 1 : poFieldDefn->GetType() == OFTDateTime))
2540 : {
2541 1 : CPLString osExpr;
2542 : {
2543 1 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2544 1 : nSecond = 0;
2545 1 : int nDateComponents = OGRWF3ParseDateTime(
2546 1 : leftExpr->papoSubExpr[1]->string_value, nYear, nMonth,
2547 : nDay, nHour, nMinute, nSecond);
2548 1 : if (nDateComponents >= 3)
2549 : {
2550 : osExpr =
2551 1 : "datetime=" +
2552 2 : SerializeDateTime(nDateComponents, nYear, nMonth,
2553 1 : nDay, nHour, nMinute, nSecond);
2554 : }
2555 : }
2556 1 : if (!osExpr.empty())
2557 : {
2558 1 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2559 1 : nSecond = 0;
2560 1 : int nDateComponents = OGRWF3ParseDateTime(
2561 1 : rightExpr->papoSubExpr[1]->string_value, nYear, nMonth,
2562 : nDay, nHour, nMinute, nSecond);
2563 1 : if (nDateComponents >= 3)
2564 : {
2565 : osExpr +=
2566 : "%2F" // '/' URL encoded
2567 2 : + SerializeDateTime(nDateComponents, nYear, nMonth,
2568 1 : nDay, nHour, nMinute, nSecond);
2569 1 : return osExpr;
2570 : }
2571 : }
2572 : }
2573 : }
2574 :
2575 : // For AND, we can deal with a failure in one of the branch
2576 : // since client-side will do that extra filtering
2577 4 : CPLString osFilter1 = BuildFilter(leftExpr);
2578 4 : CPLString osFilter2 = BuildFilter(rightExpr);
2579 2 : if (!osFilter1.empty() && !osFilter2.empty())
2580 : {
2581 2 : return osFilter1 + "&" + osFilter2;
2582 : }
2583 1 : else if (!osFilter1.empty())
2584 1 : return osFilter1;
2585 : else
2586 0 : return osFilter2;
2587 : }
2588 8 : else if (poNode->eNodeType == SNT_OPERATION &&
2589 8 : poNode->nOperation == SWQ_EQ && poNode->nSubExprCount == 2 &&
2590 5 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2591 5 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT)
2592 : {
2593 5 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2594 : const OGRFieldDefn *poFieldDefn =
2595 5 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2596 : int nDateComponents;
2597 5 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2598 5 : nSecond = 0;
2599 15 : if (m_bHasStringIdMember &&
2600 6 : strcmp(poFieldDefn->GetNameRef(), "id") == 0 &&
2601 1 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2602 : {
2603 1 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2604 : }
2605 8 : else if (poFieldDefn &&
2606 8 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2607 8 : m_aoSetQueryableAttributes.end())
2608 : {
2609 : char *pszEscapedFieldName =
2610 2 : CPLEscapeString(poFieldDefn->GetNameRef(), -1, CPLES_URL);
2611 2 : const CPLString osEscapedFieldName(pszEscapedFieldName);
2612 2 : CPLFree(pszEscapedFieldName);
2613 :
2614 2 : if (poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2615 : {
2616 2 : char *pszEscapedValue = CPLEscapeString(
2617 1 : poNode->papoSubExpr[1]->string_value, -1, CPLES_URL);
2618 2 : CPLString osRet(osEscapedFieldName);
2619 1 : osRet += "=";
2620 1 : osRet += pszEscapedValue;
2621 1 : CPLFree(pszEscapedValue);
2622 1 : return osRet;
2623 : }
2624 1 : if (poNode->papoSubExpr[1]->field_type == SWQ_INTEGER)
2625 : {
2626 2 : CPLString osRet(osEscapedFieldName);
2627 1 : osRet += "=";
2628 : osRet +=
2629 1 : CPLSPrintf(CPL_FRMT_GIB, poNode->papoSubExpr[1]->int_value);
2630 1 : return osRet;
2631 : }
2632 : }
2633 2 : else if (poFieldDefn &&
2634 4 : (poFieldDefn->GetType() == OFTDate ||
2635 2 : poFieldDefn->GetType() == OFTDateTime) &&
2636 5 : poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP &&
2637 1 : (nDateComponents = OGRWF3ParseDateTime(
2638 1 : poNode->papoSubExpr[1]->string_value, nYear, nMonth, nDay,
2639 : nHour, nMinute, nSecond)) >= 3)
2640 : {
2641 2 : return "datetime=" + SerializeDateTime(nDateComponents, nYear,
2642 : nMonth, nDay, nHour, nMinute,
2643 1 : nSecond);
2644 2 : }
2645 : }
2646 3 : else if (poNode->eNodeType == SNT_OPERATION &&
2647 3 : (poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2648 2 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE) &&
2649 2 : poNode->nSubExprCount == 2 &&
2650 2 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2651 2 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2652 2 : poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2653 : {
2654 2 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2655 : const OGRFieldDefn *poFieldDefn =
2656 2 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2657 : int nDateComponents;
2658 2 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2659 2 : nSecond = 0;
2660 2 : if (poFieldDefn &&
2661 4 : (poFieldDefn->GetType() == OFTDate ||
2662 6 : poFieldDefn->GetType() == OFTDateTime) &&
2663 2 : (nDateComponents = OGRWF3ParseDateTime(
2664 2 : poNode->papoSubExpr[1]->string_value, nYear, nMonth, nDay,
2665 : nHour, nMinute, nSecond)) >= 3)
2666 : {
2667 : CPLString osDT(SerializeDateTime(nDateComponents, nYear, nMonth,
2668 4 : nDay, nHour, nMinute, nSecond));
2669 2 : if (poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE)
2670 : {
2671 2 : return "datetime=" + osDT + "%2F..";
2672 : }
2673 : else
2674 : {
2675 2 : return "datetime=..%2F" + osDT;
2676 : }
2677 : }
2678 : }
2679 3 : m_bFilterMustBeClientSideEvaluated = true;
2680 3 : return CPLString();
2681 : }
2682 :
2683 : /************************************************************************/
2684 : /* BuildFilterCQLText() */
2685 : /************************************************************************/
2686 :
2687 0 : CPLString OGROAPIFLayer::BuildFilterCQLText(const swq_expr_node *poNode)
2688 : {
2689 0 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2690 0 : poNode->nSubExprCount == 2)
2691 : {
2692 0 : const auto leftExpr = poNode->papoSubExpr[0];
2693 0 : const auto rightExpr = poNode->papoSubExpr[1];
2694 :
2695 : // For AND, we can deal with a failure in one of the branch
2696 : // since client-side will do that extra filtering
2697 0 : CPLString osFilter1 = BuildFilterCQLText(leftExpr);
2698 0 : CPLString osFilter2 = BuildFilterCQLText(rightExpr);
2699 0 : if (!osFilter1.empty() && !osFilter2.empty())
2700 : {
2701 0 : return '(' + osFilter1 + ") AND (" + osFilter2 + ')';
2702 : }
2703 0 : else if (!osFilter1.empty())
2704 0 : return osFilter1;
2705 : else
2706 0 : return osFilter2;
2707 : }
2708 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2709 0 : poNode->nOperation == SWQ_OR && poNode->nSubExprCount == 2)
2710 : {
2711 0 : const auto leftExpr = poNode->papoSubExpr[0];
2712 0 : const auto rightExpr = poNode->papoSubExpr[1];
2713 :
2714 0 : CPLString osFilter1 = BuildFilterCQLText(leftExpr);
2715 0 : CPLString osFilter2 = BuildFilterCQLText(rightExpr);
2716 0 : if (!osFilter1.empty() && !osFilter2.empty())
2717 : {
2718 0 : return '(' + osFilter1 + ") OR (" + osFilter2 + ')';
2719 0 : }
2720 : }
2721 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2722 0 : poNode->nOperation == SWQ_NOT && poNode->nSubExprCount == 1)
2723 : {
2724 0 : const auto childExpr = poNode->papoSubExpr[0];
2725 0 : CPLString osFilterChild = BuildFilterCQLText(childExpr);
2726 0 : if (!osFilterChild.empty())
2727 : {
2728 0 : return "NOT (" + osFilterChild + ')';
2729 0 : }
2730 : }
2731 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2732 0 : poNode->nOperation == SWQ_ISNULL && poNode->nSubExprCount == 1 &&
2733 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN)
2734 : {
2735 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2736 : const OGRFieldDefn *poFieldDefn =
2737 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2738 0 : if (poFieldDefn)
2739 : {
2740 0 : return CPLString("(") + poFieldDefn->GetNameRef() + " IS NULL)";
2741 0 : }
2742 : }
2743 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2744 0 : (poNode->nOperation == SWQ_EQ || poNode->nOperation == SWQ_NE ||
2745 0 : poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2746 0 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE ||
2747 0 : poNode->nOperation == SWQ_LIKE ||
2748 0 : poNode->nOperation == SWQ_ILIKE) &&
2749 0 : poNode->nSubExprCount == 2 &&
2750 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2751 0 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT)
2752 : {
2753 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2754 : const OGRFieldDefn *poFieldDefn =
2755 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2756 0 : if (m_bHasStringIdMember && poNode->nOperation == SWQ_EQ &&
2757 0 : strcmp(poFieldDefn->GetNameRef(), "id") == 0 &&
2758 0 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2759 : {
2760 0 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2761 : }
2762 0 : else if (poFieldDefn &&
2763 0 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2764 0 : m_aoSetQueryableAttributes.end())
2765 : {
2766 0 : CPLString osRet(poFieldDefn->GetNameRef());
2767 0 : switch (poNode->nOperation)
2768 : {
2769 0 : case SWQ_EQ:
2770 0 : osRet += " = ";
2771 0 : break;
2772 0 : case SWQ_NE:
2773 0 : osRet += " <> ";
2774 0 : break;
2775 0 : case SWQ_GT:
2776 0 : osRet += " > ";
2777 0 : break;
2778 0 : case SWQ_GE:
2779 0 : osRet += " >= ";
2780 0 : break;
2781 0 : case SWQ_LT:
2782 0 : osRet += " < ";
2783 0 : break;
2784 0 : case SWQ_LE:
2785 0 : osRet += " <= ";
2786 0 : break;
2787 0 : case SWQ_LIKE:
2788 0 : osRet += " LIKE ";
2789 0 : break;
2790 0 : case SWQ_ILIKE:
2791 0 : osRet += " ILIKE ";
2792 0 : break;
2793 0 : default:
2794 0 : CPLAssert(false);
2795 : break;
2796 : }
2797 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2798 : {
2799 0 : osRet += '\'';
2800 0 : osRet += CPLString(poNode->papoSubExpr[1]->string_value)
2801 0 : .replaceAll('\'', "''");
2802 0 : osRet += '\'';
2803 0 : return osRet;
2804 : }
2805 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_INTEGER ||
2806 0 : poNode->papoSubExpr[1]->field_type == SWQ_INTEGER64)
2807 : {
2808 : osRet +=
2809 0 : CPLSPrintf(CPL_FRMT_GIB, poNode->papoSubExpr[1]->int_value);
2810 0 : return osRet;
2811 : }
2812 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_FLOAT)
2813 : {
2814 : osRet +=
2815 0 : CPLSPrintf("%.16g", poNode->papoSubExpr[1]->float_value);
2816 0 : return osRet;
2817 : }
2818 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2819 : {
2820 : int nDateComponents;
2821 0 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2822 0 : nSecond = 0;
2823 0 : if ((poFieldDefn->GetType() == OFTDate ||
2824 0 : poFieldDefn->GetType() == OFTDateTime) &&
2825 0 : (nDateComponents = OGRWF3ParseDateTime(
2826 0 : poNode->papoSubExpr[1]->string_value, nYear, nMonth,
2827 : nDay, nHour, nMinute, nSecond)) >= 3)
2828 : {
2829 : CPLString osDT(SerializeDateTime(nDateComponents, nYear,
2830 : nMonth, nDay, nHour,
2831 0 : nMinute, nSecond));
2832 0 : osRet += '\'';
2833 0 : osRet += osDT;
2834 0 : osRet += '\'';
2835 0 : return osRet;
2836 : }
2837 : }
2838 : }
2839 : }
2840 :
2841 0 : m_bFilterMustBeClientSideEvaluated = true;
2842 0 : return CPLString();
2843 : }
2844 :
2845 : /************************************************************************/
2846 : /* BuildFilterJSONFilterExpr() */
2847 : /************************************************************************/
2848 :
2849 0 : CPLString OGROAPIFLayer::BuildFilterJSONFilterExpr(const swq_expr_node *poNode)
2850 : {
2851 0 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2852 0 : poNode->nSubExprCount == 2)
2853 : {
2854 0 : const auto leftExpr = poNode->papoSubExpr[0];
2855 0 : const auto rightExpr = poNode->papoSubExpr[1];
2856 :
2857 : // For AND, we can deal with a failure in one of the branch
2858 : // since client-side will do that extra filtering
2859 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(leftExpr);
2860 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(rightExpr);
2861 0 : if (!osFilter1.empty() && !osFilter2.empty())
2862 : {
2863 0 : return "[\"all\"," + osFilter1 + ',' + osFilter2 + ']';
2864 : }
2865 0 : else if (!osFilter1.empty())
2866 0 : return osFilter1;
2867 : else
2868 0 : return osFilter2;
2869 : }
2870 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2871 0 : poNode->nOperation == SWQ_OR && poNode->nSubExprCount == 2)
2872 : {
2873 0 : const auto leftExpr = poNode->papoSubExpr[0];
2874 0 : const auto rightExpr = poNode->papoSubExpr[1];
2875 :
2876 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(leftExpr);
2877 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(rightExpr);
2878 0 : if (!osFilter1.empty() && !osFilter2.empty())
2879 : {
2880 0 : return "[\"any\"," + osFilter1 + ',' + osFilter2 + ']';
2881 0 : }
2882 : }
2883 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2884 0 : poNode->nOperation == SWQ_NOT && poNode->nSubExprCount == 1)
2885 : {
2886 0 : const auto childExpr = poNode->papoSubExpr[0];
2887 0 : CPLString osFilterChild = BuildFilterJSONFilterExpr(childExpr);
2888 0 : if (!osFilterChild.empty())
2889 : {
2890 0 : return "[\"!\"," + osFilterChild + ']';
2891 0 : }
2892 : }
2893 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2894 0 : poNode->nOperation == SWQ_ISNULL && poNode->nSubExprCount == 1)
2895 : {
2896 0 : const auto childExpr = poNode->papoSubExpr[0];
2897 0 : CPLString osFilterChild = BuildFilterJSONFilterExpr(childExpr);
2898 0 : if (!osFilterChild.empty())
2899 : {
2900 0 : return "[\"==\"," + osFilterChild + ",null]";
2901 0 : }
2902 : }
2903 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2904 0 : (poNode->nOperation == SWQ_EQ || poNode->nOperation == SWQ_NE ||
2905 0 : poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2906 0 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE ||
2907 0 : poNode->nOperation == SWQ_LIKE) &&
2908 0 : poNode->nSubExprCount == 2)
2909 : {
2910 0 : if (m_bHasStringIdMember && poNode->nOperation == SWQ_EQ &&
2911 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2912 0 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2913 0 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2914 : {
2915 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2916 : const OGRFieldDefn *poFieldDefn =
2917 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2918 0 : if (strcmp(poFieldDefn->GetNameRef(), "id") == 0)
2919 : {
2920 0 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2921 0 : return CPLString();
2922 : }
2923 : }
2924 :
2925 0 : CPLString osRet("[\"");
2926 0 : switch (poNode->nOperation)
2927 : {
2928 0 : case SWQ_EQ:
2929 0 : osRet += "==";
2930 0 : break;
2931 0 : case SWQ_NE:
2932 0 : osRet += "!=";
2933 0 : break;
2934 0 : case SWQ_GT:
2935 0 : osRet += ">";
2936 0 : break;
2937 0 : case SWQ_GE:
2938 0 : osRet += ">=";
2939 0 : break;
2940 0 : case SWQ_LT:
2941 0 : osRet += "<";
2942 0 : break;
2943 0 : case SWQ_LE:
2944 0 : osRet += "<=";
2945 0 : break;
2946 0 : case SWQ_LIKE:
2947 0 : osRet += "like";
2948 0 : break;
2949 0 : default:
2950 0 : CPLAssert(false);
2951 : break;
2952 : }
2953 0 : osRet += "\",";
2954 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(poNode->papoSubExpr[0]);
2955 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(poNode->papoSubExpr[1]);
2956 0 : if (!osFilter1.empty() && !osFilter2.empty())
2957 : {
2958 0 : osRet += osFilter1;
2959 0 : osRet += ',';
2960 0 : osRet += osFilter2;
2961 0 : osRet += ']';
2962 0 : return osRet;
2963 0 : }
2964 : }
2965 0 : else if (poNode->eNodeType == SNT_COLUMN)
2966 : {
2967 0 : const int nFieldIdx = poNode->field_index;
2968 : const OGRFieldDefn *poFieldDefn =
2969 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2970 0 : if (poFieldDefn &&
2971 0 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2972 0 : m_aoSetQueryableAttributes.end())
2973 : {
2974 0 : CPLString osRet("[\"get\",\"");
2975 0 : osRet += CPLString(poFieldDefn->GetNameRef())
2976 0 : .replaceAll('\\', "\\\\")
2977 0 : .replaceAll('"', "\\\"");
2978 0 : osRet += "\"]";
2979 0 : return osRet;
2980 : }
2981 : }
2982 0 : else if (poNode->eNodeType == SNT_CONSTANT)
2983 : {
2984 0 : if (poNode->field_type == SWQ_STRING)
2985 : {
2986 0 : CPLString osRet("\"");
2987 0 : osRet += CPLString(poNode->string_value)
2988 0 : .replaceAll('\\', "\\\\")
2989 0 : .replaceAll('"', "\\\"");
2990 0 : osRet += '"';
2991 0 : return osRet;
2992 : }
2993 0 : if (poNode->field_type == SWQ_INTEGER ||
2994 0 : poNode->field_type == SWQ_INTEGER64)
2995 : {
2996 0 : return CPLSPrintf(CPL_FRMT_GIB, poNode->int_value);
2997 : }
2998 0 : if (poNode->field_type == SWQ_FLOAT)
2999 : {
3000 0 : return CPLSPrintf("%.16g", poNode->float_value);
3001 : }
3002 0 : if (poNode->field_type == SWQ_TIMESTAMP)
3003 : {
3004 0 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
3005 0 : nSecond = 0;
3006 : const int nDateComponents =
3007 0 : OGRWF3ParseDateTime(poNode->string_value, nYear, nMonth, nDay,
3008 : nHour, nMinute, nSecond);
3009 0 : if (nDateComponents >= 3)
3010 : {
3011 : CPLString osDT(SerializeDateTime(nDateComponents, nYear, nMonth,
3012 : nDay, nHour, nMinute,
3013 0 : nSecond));
3014 0 : CPLString osRet("\"");
3015 0 : osRet += osDT;
3016 0 : osRet += '"';
3017 0 : return osRet;
3018 : }
3019 : }
3020 : }
3021 :
3022 0 : m_bFilterMustBeClientSideEvaluated = true;
3023 0 : return CPLString();
3024 : }
3025 :
3026 : /************************************************************************/
3027 : /* GetQueryableAttributes() */
3028 : /************************************************************************/
3029 :
3030 7 : void OGROAPIFLayer::GetQueryableAttributes()
3031 : {
3032 7 : if (m_bGotQueryableAttributes)
3033 6 : return;
3034 1 : m_bGotQueryableAttributes = true;
3035 1 : std::string osAPIURL;
3036 1 : CPLJSONDocument oAPIDoc = m_poDS->GetAPIDoc(osAPIURL);
3037 1 : if (oAPIDoc.GetRoot().GetString("openapi").empty())
3038 0 : return;
3039 :
3040 3 : CPLJSONObject oPaths = oAPIDoc.GetRoot().GetObj("paths");
3041 : CPLJSONArray oParameters =
3042 3 : oPaths.GetObj(m_osPath).GetObj("get").GetArray("parameters");
3043 1 : if (!oParameters.IsValid())
3044 : {
3045 0 : oParameters = oPaths.GetObj("/collections/{collectionId}/items")
3046 0 : .GetObj("get")
3047 0 : .GetArray("parameters");
3048 : }
3049 3 : for (int i = 0; i < oParameters.Size(); i++)
3050 : {
3051 4 : CPLJSONObject oParam = oParameters[i];
3052 6 : CPLString osRef = oParam.GetString("$ref");
3053 2 : if (!osRef.empty() && osRef.find("#/") == 0)
3054 : {
3055 0 : oParam = oAPIDoc.GetRoot().GetObj(osRef.substr(2));
3056 : }
3057 2 : if (oParam.GetString("in") == "query")
3058 : {
3059 6 : const auto osName(oParam.GetString("name"));
3060 2 : if (osName == "filter-lang")
3061 : {
3062 0 : const auto oEnums = oParam.GetObj("schema").GetArray("enum");
3063 0 : for (int j = 0; j < oEnums.Size(); j++)
3064 : {
3065 0 : if (oEnums[j].ToString() == "cql-text")
3066 : {
3067 0 : m_bHasCQLText = true;
3068 0 : CPLDebug("OAPIF", "CQL text detected");
3069 : }
3070 0 : else if (oEnums[j].ToString() == "json-filter-expr")
3071 : {
3072 0 : m_bHasJSONFilterExpression = true;
3073 0 : CPLDebug("OAPIF", "JSON Filter expression detected");
3074 : }
3075 : }
3076 : }
3077 2 : else if (GetLayerDefn()->GetFieldIndex(osName.c_str()) >= 0)
3078 : {
3079 2 : m_aoSetQueryableAttributes.insert(osName);
3080 : }
3081 : }
3082 : }
3083 :
3084 : // HACK
3085 1 : if (CPLTestBool(CPLGetConfigOption("OGR_OAPIF_ALLOW_CQL_TEXT", "NO")))
3086 0 : m_bHasCQLText = true;
3087 :
3088 1 : if (m_bHasCQLText || m_bHasJSONFilterExpression)
3089 : {
3090 0 : if (!m_osQueryablesURL.empty())
3091 : {
3092 0 : CPLJSONDocument oDoc;
3093 0 : if (m_poDS->DownloadJSon(m_osQueryablesURL, oDoc))
3094 : {
3095 0 : auto oQueryables = oDoc.GetRoot().GetArray("queryables");
3096 0 : for (int i = 0; i < oQueryables.Size(); i++)
3097 : {
3098 0 : const auto osId = oQueryables[i].GetString("id");
3099 0 : if (!osId.empty())
3100 : {
3101 0 : m_aoSetQueryableAttributes.insert(osId);
3102 : }
3103 : }
3104 : }
3105 : }
3106 : }
3107 : }
3108 :
3109 : /************************************************************************/
3110 : /* SetAttributeFilter() */
3111 : /************************************************************************/
3112 :
3113 9 : OGRErr OGROAPIFLayer::SetAttributeFilter(const char *pszQuery)
3114 :
3115 : {
3116 9 : if (m_poAttrQuery == nullptr && pszQuery == nullptr)
3117 1 : return OGRERR_NONE;
3118 :
3119 8 : if (!m_bFeatureDefnEstablished)
3120 1 : EstablishFeatureDefn();
3121 :
3122 8 : OGRErr eErr = OGRLayer::SetAttributeFilter(pszQuery);
3123 :
3124 8 : m_osAttributeFilter.clear();
3125 8 : m_bFilterMustBeClientSideEvaluated = false;
3126 8 : m_osGetID.clear();
3127 8 : if (m_poAttrQuery != nullptr)
3128 : {
3129 7 : GetQueryableAttributes();
3130 :
3131 7 : swq_expr_node *poNode = (swq_expr_node *)m_poAttrQuery->GetSWQExpr();
3132 :
3133 7 : poNode->ReplaceBetweenByGEAndLERecurse();
3134 :
3135 7 : if (m_bHasCQLText)
3136 : {
3137 0 : m_osAttributeFilter = BuildFilterCQLText(poNode);
3138 0 : if (!m_osAttributeFilter.empty())
3139 : {
3140 : char *pszEscaped =
3141 0 : CPLEscapeString(m_osAttributeFilter, -1, CPLES_URL);
3142 0 : m_osAttributeFilter = "filter=";
3143 0 : m_osAttributeFilter += pszEscaped;
3144 0 : m_osAttributeFilter += "&filter-lang=cql-text";
3145 0 : CPLFree(pszEscaped);
3146 : }
3147 : }
3148 7 : else if (m_bHasJSONFilterExpression)
3149 : {
3150 0 : m_osAttributeFilter = BuildFilterJSONFilterExpr(poNode);
3151 0 : if (!m_osAttributeFilter.empty())
3152 : {
3153 : char *pszEscaped =
3154 0 : CPLEscapeString(m_osAttributeFilter, -1, CPLES_URL);
3155 0 : m_osAttributeFilter = "filter=";
3156 0 : m_osAttributeFilter += pszEscaped;
3157 0 : m_osAttributeFilter += "&filter-lang=json-filter-expr";
3158 0 : CPLFree(pszEscaped);
3159 : }
3160 : }
3161 : else
3162 : {
3163 7 : m_osAttributeFilter = BuildFilter(poNode);
3164 : }
3165 7 : if (m_osAttributeFilter.empty())
3166 : {
3167 2 : CPLDebug("OAPIF", "Full filter will be evaluated on client side.");
3168 : }
3169 5 : else if (m_bFilterMustBeClientSideEvaluated)
3170 : {
3171 1 : CPLDebug(
3172 : "OAPIF",
3173 : "Only part of the filter will be evaluated on server side.");
3174 : }
3175 : }
3176 :
3177 8 : ResetReading();
3178 :
3179 8 : return eErr;
3180 : }
3181 :
3182 : /************************************************************************/
3183 : /* TestCapability() */
3184 : /************************************************************************/
3185 :
3186 11 : int OGROAPIFLayer::TestCapability(const char *pszCap)
3187 : {
3188 11 : if (EQUAL(pszCap, OLCFastFeatureCount))
3189 : {
3190 3 : return m_nTotalFeatureCount >= 0 && m_poFilterGeom == nullptr &&
3191 3 : m_poAttrQuery == nullptr;
3192 : }
3193 9 : if (EQUAL(pszCap, OLCFastGetExtent))
3194 : {
3195 1 : return m_oOriginalExtent.IsInit();
3196 : }
3197 8 : if (EQUAL(pszCap, OLCStringsAsUTF8))
3198 : {
3199 7 : return TRUE;
3200 : }
3201 : // Don't advertise OLCRandomRead as it requires a GET per feature
3202 1 : return FALSE;
3203 : }
3204 :
3205 : /************************************************************************/
3206 : /* Open() */
3207 : /************************************************************************/
3208 :
3209 41 : static GDALDataset *OGROAPIFDriverOpen(GDALOpenInfo *poOpenInfo)
3210 :
3211 : {
3212 41 : if (!OGROAPIFDriverIdentify(poOpenInfo) || poOpenInfo->eAccess == GA_Update)
3213 0 : return nullptr;
3214 82 : auto poDataset = std::make_unique<OGROAPIFDataset>();
3215 41 : if (!poDataset->Open(poOpenInfo))
3216 9 : return nullptr;
3217 32 : return poDataset.release();
3218 : }
3219 :
3220 : /************************************************************************/
3221 : /* RegisterOGROAPIF() */
3222 : /************************************************************************/
3223 :
3224 1595 : void RegisterOGROAPIF()
3225 :
3226 : {
3227 1595 : if (GDALGetDriverByName("OAPIF") != nullptr)
3228 302 : return;
3229 :
3230 1293 : GDALDriver *poDriver = new GDALDriver();
3231 :
3232 1293 : poDriver->SetDescription("OAPIF");
3233 1293 : poDriver->SetMetadataItem(GDAL_DCAP_VECTOR, "YES");
3234 1293 : poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "OGC API - Features");
3235 1293 : poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/vector/oapif.html");
3236 :
3237 1293 : poDriver->SetMetadataItem(GDAL_DMD_CONNECTION_PREFIX, "OAPIF:");
3238 1293 : poDriver->SetMetadataItem(GDAL_DMD_SUPPORTED_SQL_DIALECTS, "OGRSQL SQLITE");
3239 :
3240 1293 : poDriver->SetMetadataItem(
3241 : GDAL_DMD_OPENOPTIONLIST,
3242 : "<OpenOptionList>"
3243 : " <Option name='URL' type='string' "
3244 : "description='URL to the landing page or a /collections/{id}' "
3245 : "required='true'/>"
3246 : " <Option name='PAGE_SIZE' type='int' "
3247 : "description='Maximum number of features to retrieve in a single "
3248 : "request'/>"
3249 : " <Option name='INITIAL_REQUEST_PAGE_SIZE' type='int' "
3250 : "description='Maximum number of features to retrieve in the initial "
3251 : "request issued to determine the schema from a feature sample'/>"
3252 : " <Option name='USERPWD' type='string' "
3253 : "description='Basic authentication as username:password'/>"
3254 : " <Option name='IGNORE_SCHEMA' type='boolean' "
3255 : "description='Whether the XML Schema or JSON Schema should be ignored' "
3256 : "default='NO'/>"
3257 : " <Option name='CRS' type='string' "
3258 : "description='CRS identifier to use for layers'/>"
3259 : " <Option name='PREFERRED_CRS' type='string' "
3260 : "description='Preferred CRS identifier to use for layers'/>"
3261 : " <Option name='SERVER_FEATURE_AXIS_ORDER' type='string-select' "
3262 : "description='Coordinate axis order of GeoJSON features returned by "
3263 : "the server' "
3264 : "default='AUTHORITY_COMPLIANT'>"
3265 : " <Value>AUTHORITY_COMPLIANT</Value>"
3266 : " <Value>GIS_FRIENDLY</Value>"
3267 : " </Option>"
3268 : " <Option name='DATETIME' type='string' "
3269 : "description=\"Date-time filter to pass to items requests with the "
3270 : "'datetime' parameter\"/>"
3271 1293 : "</OpenOptionList>");
3272 :
3273 1293 : poDriver->pfnIdentify = OGROAPIFDriverIdentify;
3274 1293 : poDriver->pfnOpen = OGROAPIFDriverOpen;
3275 :
3276 1293 : GetGDALDriverManager()->RegisterDriver(poDriver);
3277 : }
|