LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/oapif - ogroapifdriver.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 1156 1583 73.0 %
Date: 2026-02-12 23:49:34 Functions: 51 53 96.2 %

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

Generated by: LCOV version 1.14