Coverage Report

Created: 2025-08-11 09:23

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