1 /*******************************************************************************
2  *  Project: NextGIS Web Driver
3  *  Purpose: Implements NextGIS Web Driver
4  *  Author: Dmitry Baryshnikov, dmitry.baryshnikov@nextgis.com
5  *  Language: C++
6  *******************************************************************************
7  *  The MIT License (MIT)
8  *
9  *  Copyright (c) 2018-2020, NextGIS <info@nextgis.com>
10  *
11  *  Permission is hereby granted, free of charge, to any person obtaining a copy
12  *  of this software and associated documentation files (the "Software"), to deal
13  *  in the Software without restriction, including without limitation the rights
14  *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15  *  copies of the Software, and to permit persons to whom the Software is
16  *  furnished to do so, subject to the following conditions:
17  *
18  *  The above copyright notice and this permission notice shall be included in all
19  *  copies or substantial portions of the Software.
20  *
21  *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24  *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25  *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26  *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27  *  SOFTWARE.
28  *******************************************************************************/
29 
30 #include "ogr_ngw.h"
31 
32 #include "cpl_http.h"
33 #include "gdal_proxy.h"
34 
35 class NGWWrapperRasterBand : public GDALProxyRasterBand
36 {
37     GDALRasterBand *poBaseBand;
38 
39 protected:
RefUnderlyingRasterBand()40     virtual GDALRasterBand *RefUnderlyingRasterBand() override { return poBaseBand; }
41 
42 public:
NGWWrapperRasterBand(GDALRasterBand * poBaseBandIn)43     explicit NGWWrapperRasterBand( GDALRasterBand* poBaseBandIn ) :
44         poBaseBand( poBaseBandIn )
45     {
46         eDataType = poBaseBand->GetRasterDataType();
47         poBaseBand->GetBlockSize(&nBlockXSize, &nBlockYSize);
48     }
~NGWWrapperRasterBand()49     virtual ~NGWWrapperRasterBand() {}
50 };
51 
52 /*
53  * OGRNGWDataset()
54  */
OGRNGWDataset()55 OGRNGWDataset::OGRNGWDataset() :
56     nBatchSize(-1),
57     nPageSize(-1),
58     bFetchedPermissions(false),
59     bHasFeaturePaging(false),
60     bExtInNativeData(false),
61     bMetadataDerty(false),
62     papoLayers(nullptr),
63     nLayers(0),
64     poRasterDS(nullptr),
65     nRasters(0),
66     nCacheExpires(604800),  // 7 days
67     nCacheMaxSize(67108864),// 64 MB
68     osJsonDepth("32")
69 {}
70 
71 /*
72  * ~OGRNGWDataset()
73  */
~OGRNGWDataset()74 OGRNGWDataset::~OGRNGWDataset()
75 {
76     // Last sync with server.
77     OGRNGWDataset::FlushCache();
78 
79     if( poRasterDS != nullptr )
80     {
81         GDALClose( poRasterDS );
82         poRasterDS = nullptr;
83     }
84 
85     for( int i = 0; i < nLayers; ++i )
86     {
87         delete papoLayers[i];
88     }
89     CPLFree( papoLayers );
90 }
91 
92 /*
93  * FetchPermissions()
94  */
FetchPermissions()95 void OGRNGWDataset::FetchPermissions()
96 {
97     if( bFetchedPermissions )
98     {
99         return;
100     }
101 
102     if( IsUpdateMode() )
103     {
104         // Check connection and is it read only.
105         char **papszHTTPOptions = GetHeaders();
106         stPermissions = NGWAPI::CheckPermissions( osUrl, osResourceId,
107             papszHTTPOptions, IsUpdateMode() );
108         CSLDestroy( papszHTTPOptions );
109     }
110     else
111     {
112         stPermissions.bDataCanRead = true;
113         stPermissions.bResourceCanRead = true;
114         stPermissions.bDatastructCanRead = true;
115         stPermissions.bMetadataCanRead = true;
116     }
117     bFetchedPermissions = true;
118 }
119 
120 /*
121  * TestCapability()
122  */
TestCapability(const char * pszCap)123 int OGRNGWDataset::TestCapability( const char *pszCap )
124 {
125     FetchPermissions();
126     if( EQUAL(pszCap, ODsCCreateLayer) )
127     {
128         return stPermissions.bResourceCanCreate;
129     }
130     else if( EQUAL(pszCap, ODsCDeleteLayer) )
131     {
132         return stPermissions.bResourceCanDelete;
133     }
134     else if( EQUAL(pszCap, "RenameLayer") )
135     {
136         return stPermissions.bResourceCanUpdate;
137     }
138     else if( EQUAL(pszCap, ODsCRandomLayerWrite) )
139     {
140         return stPermissions.bDataCanWrite; // FIXME: Check on resource level is this permission set?
141     }
142     else if( EQUAL(pszCap, ODsCRandomLayerRead) )
143     {
144         return stPermissions.bDataCanRead;
145     }
146     else
147     {
148         return FALSE;
149     }
150 }
151 
152 /*
153  * GetLayer()
154  */
GetLayer(int iLayer)155 OGRLayer *OGRNGWDataset::GetLayer( int iLayer )
156 {
157     if( iLayer < 0 || iLayer >= nLayers )
158     {
159         return nullptr;
160     }
161     else
162     {
163         return papoLayers[iLayer];
164     }
165 }
166 
167 /*
168  * Open()
169  */
Open(const std::string & osUrlIn,const std::string & osResourceIdIn,char ** papszOpenOptionsIn,bool bUpdateIn,int nOpenFlagsIn)170 bool OGRNGWDataset::Open( const std::string &osUrlIn,
171     const std::string &osResourceIdIn, char **papszOpenOptionsIn,
172     bool bUpdateIn, int nOpenFlagsIn )
173 {
174     osUrl = osUrlIn;
175     osResourceId = osResourceIdIn;
176 
177     eAccess = bUpdateIn ? GA_Update : GA_ReadOnly;
178 
179     osUserPwd = CSLFetchNameValueDef( papszOpenOptionsIn, "USERPWD",
180         CPLGetConfigOption("NGW_USERPWD", ""));
181 
182     nBatchSize = atoi( CSLFetchNameValueDef( papszOpenOptionsIn,
183         "BATCH_SIZE", CPLGetConfigOption("NGW_BATCH_SIZE", "-1") ) );
184 
185     nPageSize = atoi( CSLFetchNameValueDef(papszOpenOptionsIn, "PAGE_SIZE",
186         CPLGetConfigOption("NGW_PAGE_SIZE", "-1") ) );
187     if( nPageSize == 0 )
188     {
189         nPageSize = -1;
190     }
191 
192     nCacheExpires = atoi( CSLFetchNameValueDef(papszOpenOptionsIn, "CACHE_EXPIRES",
193         CPLGetConfigOption("NGW_CACHE_EXPIRES", "604800") ) );
194 
195     nCacheMaxSize = atoi( CSLFetchNameValueDef(papszOpenOptionsIn, "CACHE_MAX_SIZE",
196         CPLGetConfigOption("NGW_CACHE_MAX_SIZE", "67108864") ) );
197 
198     bExtInNativeData = CPLFetchBool( papszOpenOptionsIn, "NATIVE_DATA",
199         CPLTestBool( CPLGetConfigOption("NGW_NATIVE_DATA", "NO") ) );
200 
201     osJsonDepth = CSLFetchNameValueDef( papszOpenOptionsIn, "JSON_DEPTH",
202         CPLGetConfigOption("NGW_JSON_DEPTH", "32"));
203 
204     osExtensions = CSLFetchNameValueDef(papszOpenOptionsIn, "EXTENSIONS",
205         CPLGetConfigOption("NGW_EXTENSIONS", ""));
206 
207     if (osExtensions.empty())
208     {
209         bExtInNativeData = false;
210     }
211 
212     return Init( nOpenFlagsIn );
213 }
214 
215 /*
216  * Open()
217  *
218  * The pszFilename templates:
219  *      - NGW:http://some.nextgis.com/resource/0
220  *      - NGW:http://some.nextgis.com:8000/test/resource/0
221  */
Open(const char * pszFilename,char ** papszOpenOptionsIn,bool bUpdateIn,int nOpenFlagsIn)222 bool OGRNGWDataset::Open( const char *pszFilename, char **papszOpenOptionsIn,
223     bool bUpdateIn, int nOpenFlagsIn )
224 {
225     NGWAPI::Uri stUri = NGWAPI::ParseUri(pszFilename);
226 
227     if( stUri.osPrefix != "NGW" )
228     {
229         CPLError(CE_Failure, CPLE_NotSupported,
230             "Unsupported name %s", pszFilename);
231         return false;
232     }
233 
234     osUrl = stUri.osAddress;
235     osResourceId = stUri.osResourceId;
236 
237     return Open( stUri.osAddress, stUri.osResourceId, papszOpenOptionsIn,
238         bUpdateIn, nOpenFlagsIn );
239 }
240 
241 /*
242  * Init()
243  */
Init(int nOpenFlagsIn)244 bool OGRNGWDataset::Init(int nOpenFlagsIn)
245 {
246     // NOTE: Skip check API version at that moment. We expected API v3.
247 
248     // Get resource details.
249     CPLJSONDocument oResourceDetailsReq;
250     char **papszHTTPOptions = GetHeaders();
251     bool bResult = oResourceDetailsReq.LoadUrl( NGWAPI::GetResource( osUrl,
252         osResourceId ), papszHTTPOptions );
253 
254     CPLDebug("NGW", "Get resource %s details %s", osResourceId.c_str(),
255         bResult ? "success" : "failed");
256 
257     if( bResult )
258     {
259         CPLJSONObject oRoot = oResourceDetailsReq.GetRoot();
260 
261         if( oRoot.IsValid() )
262         {
263             std::string osResourceType = oRoot.GetString("resource/cls");
264             FillMetadata( oRoot );
265 
266             if( osResourceType == "resource_group" )
267             {
268                 // Check feature paging.
269                 FillCapabilities( papszHTTPOptions );
270                 if( oRoot.GetBool( "resource/children", false ) ) {
271                     // Get child resources.
272                     bResult = FillResources( papszHTTPOptions, nOpenFlagsIn );
273                 }
274             }
275             else if( (osResourceType == "vector_layer" ||
276                 osResourceType == "postgis_layer") )
277             {
278                 // Check feature paging.
279                 FillCapabilities( papszHTTPOptions );
280                 // Add vector layer.
281                 AddLayer( oRoot, papszHTTPOptions, nOpenFlagsIn );
282             }
283             else if( osResourceType == "mapserver_style" ||
284                 osResourceType == "qgis_vector_style" ||
285                 osResourceType == "raster_style" ||
286                 osResourceType == "qgis_raster_style" ||
287                 osResourceType == "wmsclient_layer" )
288             {
289                 // GetExtent from parent.
290                 OGREnvelope stExtent;
291                 std::string osParentId = oRoot.GetString("resource/parent/id");
292                 bool bExtentResult = NGWAPI::GetExtent(osUrl, osParentId,
293                     papszHTTPOptions, 3857, stExtent);
294 
295                 if( !bExtentResult )
296                 {
297                     // Set full extent for EPSG:3857.
298                     stExtent.MinX = -20037508.34;
299                     stExtent.MaxX = 20037508.34;
300                     stExtent.MinY = -20037508.34;
301                     stExtent.MaxY = 20037508.34;
302                 }
303 
304                 CPLDebug("NGW", "Raster extent is: %f, %f, %f, %f",
305                     stExtent.MinX, stExtent.MinY,
306                     stExtent.MaxX, stExtent.MaxY);
307 
308                 int nEPSG = 3857;
309                 // Get parent details. We can skip this as default SRS in NGW is 3857.
310                 if( osResourceType == "wmsclient_layer" )
311                 {
312                     nEPSG = oRoot.GetInteger("wmsclient_layer/srs/id", nEPSG);
313                 }
314                 else
315                 {
316                     CPLJSONDocument oResourceReq;
317                     bResult = oResourceReq.LoadUrl( NGWAPI::GetResource( osUrl,
318                         osResourceId ), papszHTTPOptions );
319 
320                     if( bResult )
321                     {
322                         CPLJSONObject oParentRoot = oResourceReq.GetRoot();
323                         if( osResourceType == "mapserver_style" ||
324                             osResourceType == "qgis_vector_style" )
325                         {
326                             nEPSG = oParentRoot.GetInteger("vector_layer/srs/id", nEPSG);
327                         }
328                         else if( osResourceType == "raster_style" ||
329                                  osResourceType == "qgis_raster_style")
330                         {
331                             nEPSG = oParentRoot.GetInteger("raster_layer/srs/id", nEPSG);
332                         }
333                     }
334                 }
335 
336                 // Create raster dataset.
337                 std::string osRasterUrl = NGWAPI::GetTMS(osUrl, osResourceId);
338                 char* pszRasterUrl = CPLEscapeString(osRasterUrl.c_str(), -1, CPLES_XML);
339                 const char *pszConnStr = CPLSPrintf("<GDAL_WMS><Service name=\"TMS\">"
340             "<ServerUrl>%s</ServerUrl></Service><DataWindow>"
341             "<UpperLeftX>-20037508.34</UpperLeftX><UpperLeftY>20037508.34</UpperLeftY>"
342             "<LowerRightX>20037508.34</LowerRightX><LowerRightY>-20037508.34</LowerRightY>"
343             "<TileLevel>%d</TileLevel><TileCountX>1</TileCountX>"
344             "<TileCountY>1</TileCountY><YOrigin>top</YOrigin></DataWindow>"
345             "<Projection>EPSG:%d</Projection><BlockSizeX>256</BlockSizeX>"
346             "<BlockSizeY>256</BlockSizeY><BandsCount>%d</BandsCount>"
347             "<Cache><Type>file</Type><Expires>%d</Expires><MaxSize>%d</MaxSize>"
348             "</Cache><ZeroBlockHttpCodes>204,404</ZeroBlockHttpCodes></GDAL_WMS>",
349                 pszRasterUrl,
350                 22,      // NOTE: We have no limit in zoom levels.
351                 nEPSG,   // NOTE: Default SRS is EPSG:3857.
352                 4,
353                 nCacheExpires,
354                 nCacheMaxSize);
355 
356                 CPLFree( pszRasterUrl );
357 
358                 poRasterDS = reinterpret_cast<GDALDataset*>(GDALOpenEx(pszConnStr,
359                     GDAL_OF_READONLY | GDAL_OF_RASTER | GDAL_OF_INTERNAL, nullptr,
360                     nullptr, nullptr));
361 
362                 if( poRasterDS )
363                 {
364                     bResult = true;
365                     nRasterXSize = poRasterDS->GetRasterXSize();
366                     nRasterYSize = poRasterDS->GetRasterYSize();
367 
368                     for( int iBand = 1; iBand <= poRasterDS->GetRasterCount();
369                             iBand++ )
370                     {
371                         SetBand( iBand, new NGWWrapperRasterBand(
372                             poRasterDS->GetRasterBand( iBand )) );
373                     }
374 
375                     // Set pixel limits.
376                     bool bHasTransform = false;
377                     double geoTransform[6] = { 0.0 };
378                     double invGeoTransform[6] = { 0.0 };
379                     if(poRasterDS->GetGeoTransform(geoTransform) == CE_None)
380                     {
381                         bHasTransform = GDALInvGeoTransform(geoTransform,
382                             invGeoTransform) == TRUE;
383                     }
384 
385                     if(bHasTransform)
386                     {
387                         GDALApplyGeoTransform(invGeoTransform, stExtent.MinX,
388                             stExtent.MinY, &stPixelExtent.MinX, &stPixelExtent.MaxY);
389 
390                         GDALApplyGeoTransform(invGeoTransform, stExtent.MaxX,
391                             stExtent.MaxY, &stPixelExtent.MaxX, &stPixelExtent.MinY);
392 
393                         CPLDebug("NGW", "Raster extent in px is: %f, %f, %f, %f",
394                             stPixelExtent.MinX, stPixelExtent.MinY,
395                             stPixelExtent.MaxX, stPixelExtent.MaxY);
396                     }
397                     else
398                     {
399                         stPixelExtent.MinX = 0.0;
400                         stPixelExtent.MinY = 0.0;
401                         stPixelExtent.MaxX = std::numeric_limits<double>::max();
402                         stPixelExtent.MaxY = std::numeric_limits<double>::max();
403                     }
404                 }
405                 else
406                 {
407                     bResult = false;
408                 }
409             }
410             else if( osResourceType == "raster_layer" ) //FIXME: Do we need this check? && nOpenFlagsIn & GDAL_OF_RASTER )
411             {
412                 AddRaster( oRoot, papszHTTPOptions );
413             }
414             else
415             {
416                 bResult = false;
417             }
418             // TODO: Add support for baselayers, webmap, wfsserver_service, wmsserver_service.
419         }
420     }
421 
422     CSLDestroy( papszHTTPOptions );
423     return bResult;
424 }
425 
426 /*
427  * FillResources()
428  */
FillResources(char ** papszOptions,int nOpenFlagsIn)429 bool OGRNGWDataset::FillResources( char **papszOptions, int nOpenFlagsIn )
430 {
431     CPLJSONDocument oResourceDetailsReq;
432     bool bResult = oResourceDetailsReq.LoadUrl( NGWAPI::GetChildren( osUrl,
433         osResourceId ), papszOptions );
434 
435     if( bResult )
436     {
437         CPLJSONArray oChildren(oResourceDetailsReq.GetRoot());
438         for( int i = 0; i < oChildren.Size(); ++i )
439         {
440             CPLJSONObject oChild = oChildren[i];
441             std::string osResourceType = oChild.GetString("resource/cls");
442             if( (osResourceType == "vector_layer" ||
443                 osResourceType == "postgis_layer") )
444             {
445                 // Add vector layer. If failed, try next layer.
446                 AddLayer( oChild, papszOptions, nOpenFlagsIn );
447             }
448             else if( (osResourceType == "raster_layer" ||
449                 osResourceType == "wmsclient_layer") && nOpenFlagsIn & GDAL_OF_RASTER )
450             {
451                 AddRaster( oChild, papszOptions );
452             }
453             // TODO: Add support for baselayers, webmap, wfsserver_service, wmsserver_service.
454         }
455     }
456     return bResult;
457 }
458 
459 /*
460  * AddLayer()
461  */
AddLayer(const CPLJSONObject & oResourceJsonObject,char ** papszOptions,int nOpenFlagsIn)462 void OGRNGWDataset::AddLayer( const CPLJSONObject &oResourceJsonObject,
463     char **papszOptions, int nOpenFlagsIn )
464 {
465     std::string osLayerResourceId;
466     if( nOpenFlagsIn & GDAL_OF_VECTOR )
467     {
468         OGRNGWLayer *poLayer = new OGRNGWLayer( this, oResourceJsonObject );
469         papoLayers = (OGRNGWLayer**) CPLRealloc(papoLayers, (nLayers + 1) *
470             sizeof(OGRNGWLayer*));
471         papoLayers[nLayers++] = poLayer;
472         osLayerResourceId = poLayer->GetResourceId();
473     }
474     else
475     {
476         osLayerResourceId = oResourceJsonObject.GetString("resource/id");
477     }
478 
479     // Check styles exist and add them as rasters.
480     if( nOpenFlagsIn & GDAL_OF_RASTER &&
481         oResourceJsonObject.GetBool( "resource/children", false ) )
482     {
483         CPLJSONDocument oResourceChildReq;
484         bool bResult = oResourceChildReq.LoadUrl( NGWAPI::GetChildren( osUrl,
485             osLayerResourceId ), papszOptions );
486 
487         if( bResult )
488         {
489             CPLJSONArray oChildren( oResourceChildReq.GetRoot() );
490             for( int i = 0; i < oChildren.Size(); ++i )
491             {
492                 AddRaster( oChildren[i], papszOptions );
493             }
494         }
495     }
496 }
497 
498 /*
499  * AddRaster()
500  */
AddRaster(const CPLJSONObject & oRasterJsonObj,char ** papszOptions)501 void OGRNGWDataset::AddRaster( const CPLJSONObject &oRasterJsonObj,
502     char **papszOptions )
503 {
504     std::string osOutResourceId;
505     std::string osOutResourceName;
506     std::string osResourceType = oRasterJsonObj.GetString( "resource/cls" );
507     if( osResourceType == "mapserver_style" ||
508         osResourceType == "qgis_vector_style" ||
509         osResourceType == "raster_style" ||
510         osResourceType == "qgis_raster_style" ||
511         osResourceType == "wmsclient_layer" )
512     {
513         osOutResourceId = oRasterJsonObj.GetString( "resource/id" );
514         osOutResourceName = oRasterJsonObj.GetString( "resource/display_name" );
515     }
516     else if( osResourceType == "raster_layer" )
517     {
518         std::string osRasterResourceId = oRasterJsonObj.GetString( "resource/id" );
519         CPLJSONDocument oResourceRequest;
520         bool bResult = oResourceRequest.LoadUrl( NGWAPI::GetChildren( osUrl,
521             osRasterResourceId ), papszOptions );
522 
523         if( bResult )
524         {
525             CPLJSONArray oChildren(oResourceRequest.GetRoot());
526             for( int i = 0; i < oChildren.Size(); ++i )
527             {
528                 CPLJSONObject oChild = oChildren[i];
529                 osResourceType = oChild.GetString("resource/cls");
530                 if( osResourceType == "raster_style" ||
531                     osResourceType == "qgis_raster_style" )
532                 {
533                     AddRaster( oChild, papszOptions );
534                 }
535             }
536         }
537     }
538 
539     if( !osOutResourceId.empty() )
540     {
541         if( osOutResourceName.empty() )
542         {
543             osOutResourceName = "raster_" + osOutResourceId;
544         }
545 
546         CPLDebug("NGW", "Add raster %s: %s", osOutResourceId.c_str(),
547             osOutResourceName.c_str());
548 
549         GDALDataset::SetMetadataItem( CPLSPrintf("SUBDATASET_%d_NAME", nRasters),
550             CPLSPrintf("NGW:%s/resource/%s", osUrl.c_str(),
551             osOutResourceId.c_str()), "SUBDATASETS" );
552         GDALDataset::SetMetadataItem( CPLSPrintf("SUBDATASET_%d_DESC", nRasters),
553             osOutResourceName.c_str(), "SUBDATASETS" );
554         nRasters++;
555     }
556 }
557 
558 /*
559  * ICreateLayer
560  */
ICreateLayer(const char * pszNameIn,OGRSpatialReference * poSpatialRef,OGRwkbGeometryType eGType,char ** papszOptions)561 OGRLayer *OGRNGWDataset::ICreateLayer( const char *pszNameIn,
562                                            OGRSpatialReference *poSpatialRef,
563                                            OGRwkbGeometryType eGType,
564                                            char **papszOptions )
565 {
566     if( !IsUpdateMode() )
567     {
568         CPLError(CE_Failure, CPLE_AppDefined,
569             "Operation not available in read-only mode");
570         return nullptr;
571     }
572 
573     // Check permissions as we create new layer in memory and will create in during SyncToDisk.
574     FetchPermissions();
575 
576     if( !stPermissions.bResourceCanCreate )
577     {
578         CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
579         return nullptr;
580     }
581 
582     // Check input parameters.
583     if( (eGType < wkbPoint || eGType > wkbMultiPolygon) &&
584         (eGType < wkbPoint25D || eGType > wkbMultiPolygon25D) )
585     {
586         CPLError(CE_Failure, CPLE_AppDefined,
587             "Unsupported geometry type: %s", OGRGeometryTypeToName(eGType));
588         return nullptr;
589     }
590 
591     if( !poSpatialRef )
592     {
593         CPLError(CE_Failure, CPLE_AppDefined, "Undefined spatial reference");
594         return nullptr;
595     }
596 
597     poSpatialRef->AutoIdentifyEPSG();
598     const char *pszEPSG = poSpatialRef->GetAuthorityCode( nullptr );
599     int nEPSG = -1;
600     if( pszEPSG != nullptr )
601     {
602         nEPSG = atoi( pszEPSG );
603     }
604 
605     if( nEPSG != 3857 ) // TODO: Check NextGIS Web supported SRS.
606     {
607         CPLError(CE_Failure, CPLE_AppDefined,
608             "Unsupported spatial reference EPSG code: %d", nEPSG);
609         return nullptr;
610     }
611 
612     // Do we already have this layer?  If so, should we blow it away?
613     bool bOverwrite = CPLFetchBool(papszOptions, "OVERWRITE", false);
614     for( int iLayer = 0; iLayer < nLayers; ++iLayer )
615     {
616         if( EQUAL(pszNameIn, papoLayers[iLayer]->GetName()) )
617         {
618             if( bOverwrite )
619             {
620                 DeleteLayer( iLayer );
621                 break;
622             }
623             else
624             {
625                 CPLError( CE_Failure, CPLE_AppDefined,
626                           "Layer %s already exists, CreateLayer failed.\n"
627                           "Use the layer creation option OVERWRITE=YES to "
628                           "replace it.",
629                           pszNameIn );
630                 return nullptr;
631             }
632         }
633     }
634 
635     // Create layer.
636     std::string osKey = CSLFetchNameValueDef( papszOptions, "KEY", "");
637     std::string osDesc = CSLFetchNameValueDef( papszOptions, "DESCRIPTION", "");
638     OGRSpatialReference* poSRSClone = poSpatialRef->Clone();
639     poSRSClone->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
640     OGRNGWLayer *poLayer = new OGRNGWLayer( this, pszNameIn, poSRSClone, eGType,
641         osKey, osDesc );
642     poSRSClone->Release();
643     papoLayers = (OGRNGWLayer**) CPLRealloc(papoLayers, (nLayers + 1) *
644         sizeof(OGRNGWLayer*));
645     papoLayers[nLayers++] = poLayer;
646     return poLayer;
647 }
648 
649 /*
650  * DeleteLayer()
651  */
DeleteLayer(int iLayer)652 OGRErr OGRNGWDataset::DeleteLayer( int iLayer )
653 {
654     if( !IsUpdateMode() )
655     {
656         CPLError(CE_Failure, CPLE_AppDefined,
657             "Operation not available in read-only mode.");
658         return OGRERR_FAILURE;
659     }
660 
661     if( iLayer < 0 || iLayer >= nLayers )
662     {
663         CPLError( CE_Failure, CPLE_AppDefined,
664             "Layer %d not in legal range of 0 to %d.", iLayer, nLayers-1 );
665         return OGRERR_FAILURE;
666     }
667 
668     OGRNGWLayer *poLayer = static_cast<OGRNGWLayer*>(papoLayers[iLayer]);
669 
670     if( poLayer->GetResourceId() != "-1" )
671     {
672         // For layers from server we can check permissions.
673 
674         // We can skip check permissions here as papoLayers[iLayer]->Delete() will
675         // return false if no delete permission available.
676         FetchPermissions();
677 
678         if( !stPermissions.bResourceCanDelete )
679         {
680             CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
681             return OGRERR_FAILURE;
682         }
683     }
684 
685     if( poLayer->Delete() )
686     {
687         delete poLayer;
688         memmove( papoLayers + iLayer, papoLayers + iLayer + 1,
689                  sizeof(void *) * (nLayers - iLayer - 1) );
690         nLayers--;
691     }
692 
693     return OGRERR_NONE;
694 }
695 
696 /*
697  * FillMetadata()
698  */
FillMetadata(const CPLJSONObject & oRootObject)699 void OGRNGWDataset::FillMetadata( const CPLJSONObject &oRootObject )
700 {
701     std::string osCreateDate = oRootObject.GetString("resource/creation_date");
702     if( !osCreateDate.empty() )
703     {
704         GDALDataset::SetMetadataItem( "creation_date", osCreateDate.c_str() );
705     }
706     osName = oRootObject.GetString("resource/display_name");
707     SetDescription( osName.c_str() );
708     GDALDataset::SetMetadataItem( "display_name", osName.c_str() );
709     std::string osDescription = oRootObject.GetString("resource/description");
710     if( !osDescription.empty() )
711     {
712         GDALDataset::SetMetadataItem( "description", osDescription.c_str() );
713     }
714     std::string osResourceType = oRootObject.GetString("resource/cls");
715     if( !osResourceType.empty() )
716     {
717         GDALDataset::SetMetadataItem( "resource_type", osResourceType.c_str() );
718     }
719     std::string osResourceParentId = oRootObject.GetString("resource/parent/id");
720     if( !osResourceParentId.empty() )
721     {
722         GDALDataset::SetMetadataItem( "parent_id", osResourceParentId.c_str() );
723     }
724     GDALDataset::SetMetadataItem( "id", osResourceId.c_str() );
725 
726     std::vector<CPLJSONObject> items =
727         oRootObject.GetObj("resmeta/items").GetChildren();
728 
729     for( const CPLJSONObject &item : items )
730     {
731         std::string osSuffix = NGWAPI::GetResmetaSuffix( item.GetType() );
732         GDALDataset::SetMetadataItem( (item.GetName() + osSuffix).c_str(),
733             item.ToString().c_str(), "NGW" );
734     }
735 }
736 
737 /*
738  * FlushMetadata()
739  */
FlushMetadata(char ** papszMetadata)740 bool OGRNGWDataset::FlushMetadata( char **papszMetadata )
741 {
742     if( !bMetadataDerty )
743     {
744         return true;
745     }
746 
747     bool bResult = NGWAPI::FlushMetadata(osUrl, osResourceId, papszMetadata,
748         GetHeaders());
749     if( bResult )
750     {
751         bMetadataDerty = false;
752     }
753 
754     return bResult;
755 }
756 
757 /*
758  * SetMetadata()
759  */
SetMetadata(char ** papszMetadata,const char * pszDomain)760 CPLErr OGRNGWDataset::SetMetadata( char **papszMetadata, const char *pszDomain)
761 {
762     FetchPermissions();
763     if( !stPermissions.bMetadataCanWrite )
764     {
765         CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
766         return CE_Failure;
767     }
768 
769     CPLErr eResult = GDALDataset::SetMetadata(papszMetadata, pszDomain);
770     if( eResult == CE_None && pszDomain != nullptr && EQUAL(pszDomain, "NGW") )
771     {
772         eResult = FlushMetadata( papszMetadata ) ? CE_None : CE_Failure;
773     }
774     return eResult;
775 }
776 
777 /*
778  * SetMetadataItem()
779  */
SetMetadataItem(const char * pszName,const char * pszValue,const char * pszDomain)780 CPLErr OGRNGWDataset::SetMetadataItem( const char *pszName,
781     const char *pszValue, const char *pszDomain)
782 {
783     FetchPermissions();
784     if( !stPermissions.bMetadataCanWrite )
785     {
786         CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
787         return CE_Failure;
788     }
789     if( pszDomain != nullptr && EQUAL(pszDomain, "NGW") )
790     {
791         bMetadataDerty = true;
792     }
793     return GDALDataset::SetMetadataItem(pszName, pszValue, pszDomain);
794 }
795 
796 /*
797  * FlushCache()
798  */
FlushCache()799 void OGRNGWDataset::FlushCache()
800 {
801     GDALDataset::FlushCache();
802     FlushMetadata( GetMetadata("NGW") );
803 }
804 
805 /*
806  * GetHeaders()
807  */
GetHeaders() const808 char **OGRNGWDataset::GetHeaders() const
809 {
810     char **papszOptions = nullptr;
811     papszOptions = CSLAddString(papszOptions, "HEADERS=Accept: */*");
812     papszOptions = CSLAddNameValue(papszOptions, "JSON_DEPTH", osJsonDepth.c_str());
813     if( !osUserPwd.empty() )
814     {
815         papszOptions = CSLAddString(papszOptions, "HTTPAUTH=BASIC");
816         std::string osUserPwdOption("USERPWD=");
817         osUserPwdOption += osUserPwd;
818         papszOptions = CSLAddString(papszOptions, osUserPwdOption.c_str());
819     }
820     return papszOptions;
821 }
822 
823 /*
824  * SQLUnescape()
825  * Get from gdal/ogr/ogrsf_frmts/sqlite/ogrsqliteutility.cpp as we don't want
826  * dependency on sqlite
827  */
SQLUnescape(const char * pszVal)828 static CPLString SQLUnescape( const char *pszVal )
829 {
830     char chQuoteChar = pszVal[0];
831     if( chQuoteChar != '\'' && chQuoteChar != '"' )
832         return pszVal;
833 
834     CPLString osRet;
835     pszVal ++;
836     while( *pszVal != '\0' )
837     {
838         if( *pszVal == chQuoteChar )
839         {
840             if( pszVal[1] == chQuoteChar )
841                 pszVal ++;
842             else
843                 break;
844         }
845         osRet += *pszVal;
846         pszVal ++;
847     }
848     return osRet;
849 }
850 
851 /*
852  * SQLTokenize()
853  * Get from gdal/ogr/ogrsf_frmts/sqlite/ogrsqliteutility.cpp as we don't want
854  * dependency on sqlite
855  */
SQLTokenize(const char * pszStr)856 static char **SQLTokenize( const char *pszStr )
857 {
858     char** papszTokens = nullptr;
859     bool bInQuote = false;
860     char chQuoteChar = '\0';
861     bool bInSpace = true;
862     CPLString osCurrentToken;
863     while( *pszStr != '\0' )
864     {
865         if( *pszStr == ' ' && !bInQuote )
866         {
867             if( !bInSpace )
868             {
869                 papszTokens = CSLAddString(papszTokens, osCurrentToken);
870                 osCurrentToken.clear();
871             }
872             bInSpace = true;
873         }
874         else if( (*pszStr == '(' || *pszStr == ')' || *pszStr == ',')  && !bInQuote )
875         {
876             if( !bInSpace )
877             {
878                 papszTokens = CSLAddString(papszTokens, osCurrentToken);
879                 osCurrentToken.clear();
880             }
881             osCurrentToken.clear();
882             osCurrentToken += *pszStr;
883             papszTokens = CSLAddString(papszTokens, osCurrentToken);
884             osCurrentToken.clear();
885             bInSpace = true;
886         }
887         else if( *pszStr == '"' || *pszStr == '\'' )
888         {
889             if( bInQuote && *pszStr == chQuoteChar && pszStr[1] == chQuoteChar )
890             {
891                 osCurrentToken += *pszStr;
892                 osCurrentToken += *pszStr;
893                 pszStr += 2;
894                 continue;
895             }
896             else if( bInQuote && *pszStr == chQuoteChar )
897             {
898                 osCurrentToken += *pszStr;
899                 papszTokens = CSLAddString(papszTokens, osCurrentToken);
900                 osCurrentToken.clear();
901                 bInSpace = true;
902                 bInQuote = false;
903                 chQuoteChar = '\0';
904             }
905             else if( bInQuote )
906             {
907                 osCurrentToken += *pszStr;
908             }
909             else
910             {
911                 chQuoteChar = *pszStr;
912                 osCurrentToken.clear();
913                 osCurrentToken += chQuoteChar;
914                 bInQuote = true;
915                 bInSpace = false;
916             }
917         }
918         else
919         {
920             osCurrentToken += *pszStr;
921             bInSpace = false;
922         }
923         pszStr ++;
924     }
925 
926     if( !osCurrentToken.empty() )
927         papszTokens = CSLAddString(papszTokens, osCurrentToken);
928 
929     return papszTokens;
930 }
931 
932 
933 /*
934  * ExecuteSQL()
935  */
ExecuteSQL(const char * pszStatement,OGRGeometry * poSpatialFilter,const char * pszDialect)936 OGRLayer *OGRNGWDataset::ExecuteSQL( const char *pszStatement,
937     OGRGeometry *poSpatialFilter, const char *pszDialect )
938 {
939     // Clean statement string.
940     CPLString osStatement(pszStatement);
941     osStatement = osStatement.Trim().replaceAll("  ", " ");
942 
943     if( STARTS_WITH_CI(osStatement, "DELLAYER:") )
944     {
945         CPLString osLayerName = osStatement.substr(strlen("DELLAYER:"));
946         if( osLayerName.endsWith(";") )
947         {
948             osLayerName = osLayerName.substr(0, osLayerName.size() - 1);
949             osLayerName.Trim();
950         }
951 
952         CPLDebug("NGW", "Delete layer with name %s.", osLayerName.c_str());
953 
954         for( int iLayer = 0; iLayer < nLayers; ++iLayer )
955         {
956             if( EQUAL(papoLayers[iLayer]->GetName(), osLayerName ) )
957             {
958                 DeleteLayer( iLayer );
959                 return nullptr;
960             }
961         }
962         CPLError(CE_Failure, CPLE_AppDefined, "Unknown layer : %s",
963             osLayerName.c_str());
964 
965         return nullptr;
966     }
967 
968     if( STARTS_WITH_CI(osStatement, "DELETE FROM") )
969     {
970         // Get layer name from pszStatement DELETE FROM layer;.
971         CPLString osLayerName = osStatement.substr(strlen("DELETE FROM "));
972         if( osLayerName.endsWith(";") )
973         {
974             osLayerName = osLayerName.substr(0, osLayerName.size() - 1);
975             osLayerName.Trim();
976         }
977 
978         CPLDebug("NGW", "Delete features from layer with name %s.", osLayerName.c_str());
979 
980         OGRNGWLayer *poLayer = static_cast<OGRNGWLayer*>(GetLayerByName(osLayerName));
981         if( poLayer )
982         {
983             poLayer->DeleteAllFeatures();
984         }
985         else
986         {
987             CPLError(CE_Failure, CPLE_AppDefined, "Unknown layer : %s",
988                 osLayerName.c_str());
989         }
990         return nullptr;
991     }
992 
993     if( STARTS_WITH_CI(osStatement, "DROP TABLE") )
994     {
995         // Get layer name from pszStatement DELETE FROM layer;.
996         CPLString osLayerName = osStatement.substr(strlen("DROP TABLE "));
997         if( osLayerName.endsWith(";") )
998         {
999             osLayerName = osLayerName.substr(0, osLayerName.size() - 1);
1000             osLayerName.Trim();
1001         }
1002 
1003         CPLDebug("NGW", "Delete layer with name %s.", osLayerName.c_str());
1004 
1005         for( int iLayer = 0; iLayer < nLayers; ++iLayer )
1006         {
1007             if( EQUAL(papoLayers[iLayer]->GetName(), osLayerName ) )
1008             {
1009                 DeleteLayer( iLayer );
1010                 return nullptr;
1011             }
1012         }
1013 
1014         CPLError(CE_Failure, CPLE_AppDefined, "Unknown layer : %s",
1015             osLayerName.c_str());
1016 
1017         return nullptr;
1018     }
1019 
1020     if( STARTS_WITH_CI(osStatement, "ALTER TABLE ") )
1021     {
1022         if( osStatement.endsWith(";") )
1023         {
1024             osStatement = osStatement.substr(0, osStatement.size() - 1);
1025             osStatement.Trim();
1026         }
1027 
1028         CPLStringList aosTokens( SQLTokenize(osStatement) );
1029         /* ALTER TABLE src_table RENAME TO dst_table */
1030         if( aosTokens.size() == 6 && EQUAL(aosTokens[3], "RENAME") &&
1031             EQUAL(aosTokens[4], "TO") )
1032         {
1033             const char* pszSrcTableName = aosTokens[2];
1034             const char* pszDstTableName = aosTokens[5];
1035 
1036             OGRNGWLayer *poLayer = static_cast<OGRNGWLayer*>(GetLayerByName(
1037                 SQLUnescape(pszSrcTableName) ));
1038             if( poLayer )
1039             {
1040                 poLayer->Rename( SQLUnescape(pszDstTableName) );
1041                 return nullptr;
1042             }
1043 
1044             CPLError(CE_Failure, CPLE_AppDefined, "Unknown layer : %s",
1045                 pszSrcTableName);
1046         }
1047         else
1048         {
1049             CPLError(CE_Failure, CPLE_AppDefined,
1050                 "Unsupported alter table operation. Only rename table to ... support.");
1051         }
1052         return nullptr;
1053     }
1054 
1055     // SELECT xxxxx FROM yyyy WHERE zzzzzz;
1056     if( STARTS_WITH_CI(osStatement, "SELECT ") )
1057     {
1058         swq_select oSelect;
1059         CPLDebug("NGW", "Select statement: %s", osStatement.c_str());
1060         if( oSelect.preparse( osStatement ) != CE_None )
1061         {
1062             return nullptr;
1063         }
1064 
1065         if( oSelect.join_count == 0 && oSelect.poOtherSelect == nullptr &&
1066             oSelect.table_count == 1 && oSelect.order_specs == 0 )
1067         {
1068             OGRNGWLayer *poLayer = reinterpret_cast<OGRNGWLayer*>(
1069                 GetLayerByName( oSelect.table_defs[0].table_name ) );
1070             if( nullptr == poLayer )
1071             {
1072                 CPLError(CE_Failure, CPLE_AppDefined,
1073                     "Layer %s not found in dataset.", oSelect.table_defs[0].table_name);
1074                 return nullptr;
1075             }
1076 
1077             std::set<std::string> aosFields;
1078             bool bSkip = false;
1079             for( int i = 0; i < oSelect.result_columns; ++i )
1080             {
1081                 swq_col_func col_func = oSelect.column_defs[i].col_func;
1082                 if( col_func != SWQCF_NONE )
1083                 {
1084                     bSkip = true;
1085                     break;
1086                 }
1087 
1088                 if( oSelect.column_defs[i].distinct_flag )
1089                 {
1090                     CPLError(CE_Warning, CPLE_AppDefined,
1091                         "Distinct not supported.");
1092                     bSkip = true;
1093                     break;
1094                 }
1095 
1096                 if( oSelect.column_defs[i].field_name != nullptr )
1097                 {
1098                     if( EQUAL(oSelect.column_defs[i].field_name, "*") )
1099                     {
1100                         aosFields.clear();
1101                         aosFields.emplace( oSelect.column_defs[i].field_name );
1102                         break;
1103                     }
1104                     else
1105                     {
1106                         aosFields.emplace( oSelect.column_defs[i].field_name );
1107                     }
1108                 }
1109             }
1110 
1111             std::string osNgwSelect;
1112             for( int iKey = 0; iKey < oSelect.order_specs; iKey++ )
1113             {
1114                 swq_order_def *psKeyDef = oSelect.order_defs + iKey;
1115                 if(iKey > 0 )
1116                 {
1117                     osNgwSelect += ",";
1118                 }
1119 
1120                 if( psKeyDef->ascending_flag == TRUE )
1121                 {
1122                     osNgwSelect += psKeyDef->field_name;
1123                 }
1124                 else
1125                 {
1126                     osNgwSelect += "-" + std::string(psKeyDef->field_name);
1127                 }
1128             }
1129 
1130             if( oSelect.where_expr != nullptr )
1131             {
1132                 if( !osNgwSelect.empty() )
1133                 {
1134                     osNgwSelect += "&";
1135                 }
1136                 osNgwSelect += OGRNGWLayer::TranslateSQLToFilter(
1137                     oSelect.where_expr );
1138 
1139             }
1140 
1141             if( osNgwSelect.empty() )
1142             {
1143                 bSkip = true;
1144             }
1145 
1146             if( !bSkip )
1147             {
1148                 if( aosFields.empty() )
1149                 {
1150                     CPLError(CE_Failure, CPLE_AppDefined,
1151                         "SELECT statement is invalid: field list is empty.");
1152                     return nullptr;
1153                 }
1154 
1155                 if( poLayer->SyncToDisk() != OGRERR_NONE )
1156                 {
1157                     return nullptr;
1158                 }
1159 
1160                 OGRNGWLayer *poOutLayer = poLayer->Clone();
1161                 if( aosFields.size() == 1 && *(aosFields.begin()) == "*" )
1162                 {
1163                     poOutLayer->SetIgnoredFields(nullptr);
1164                 }
1165                 else
1166                 {
1167                     poOutLayer->SetSelectedFields(aosFields);
1168                 }
1169                 poOutLayer->SetSpatialFilter(poSpatialFilter);
1170 
1171                 if( osNgwSelect.empty() ) // If we here oSelect.where_expr is empty
1172                 {
1173                     poOutLayer->SetAttributeFilter(nullptr);
1174                 }
1175                 else
1176                 {
1177                     std::string osAttributeFilte = "NGW:" + osNgwSelect;
1178                     poOutLayer->SetAttributeFilter(osAttributeFilte.c_str());
1179                 }
1180                 return poOutLayer;
1181             }
1182         }
1183     }
1184 
1185     return GDALDataset::ExecuteSQL(pszStatement, poSpatialFilter, pszDialect);
1186 }
1187 
1188 /*
1189  * GetProjectionRef()
1190  */
GetSpatialRef() const1191 const OGRSpatialReference *OGRNGWDataset::GetSpatialRef() const
1192 {
1193     if( poRasterDS != nullptr )
1194     {
1195         return poRasterDS->GetSpatialRef();
1196     }
1197     return GDALDataset::GetSpatialRef();
1198 }
1199 
1200 /*
1201  * GetGeoTransform()
1202  */
GetGeoTransform(double * padfTransform)1203 CPLErr OGRNGWDataset::GetGeoTransform( double *padfTransform )
1204 {
1205     if( poRasterDS != nullptr )
1206     {
1207         return poRasterDS->GetGeoTransform( padfTransform );
1208     }
1209     return GDALDataset::GetGeoTransform( padfTransform );
1210 }
1211 
1212 /*
1213  * IRasterIO()
1214  */
IRasterIO(GDALRWFlag eRWFlag,int nXOff,int nYOff,int nXSize,int nYSize,void * pData,int nBufXSize,int nBufYSize,GDALDataType eBufType,int nBandCount,int * panBandMap,GSpacing nPixelSpace,GSpacing nLineSpace,GSpacing nBandSpace,GDALRasterIOExtraArg * psExtraArg)1215 CPLErr OGRNGWDataset::IRasterIO( GDALRWFlag eRWFlag, int nXOff, int nYOff,
1216     int nXSize, int nYSize, void *pData, int nBufXSize, int nBufYSize,
1217     GDALDataType eBufType, int nBandCount, int *panBandMap,
1218     GSpacing nPixelSpace, GSpacing nLineSpace, GSpacing nBandSpace,
1219     GDALRasterIOExtraArg* psExtraArg )
1220 {
1221     if( poRasterDS != nullptr )
1222     {
1223         if( stPixelExtent.IsInit() )
1224         {
1225             OGREnvelope stTestExtent;
1226             stTestExtent.MinX = static_cast<double>(nXOff);
1227             stTestExtent.MinY = static_cast<double>(nYOff);
1228             stTestExtent.MaxX = static_cast<double>(nXOff + nXSize);
1229             stTestExtent.MaxY = static_cast<double>(nYOff + nYSize);
1230 
1231             if( !stPixelExtent.Intersects(stTestExtent) )
1232             {
1233                 CPLDebug("NGW", "Raster extent in px is: %f, %f, %f, %f",
1234                     stPixelExtent.MinX, stPixelExtent.MinY,
1235                     stPixelExtent.MaxX, stPixelExtent.MaxY);
1236                 CPLDebug("NGW", "RasterIO extent is: %f, %f, %f, %f",
1237                     stTestExtent.MinX, stTestExtent.MinY,
1238                     stTestExtent.MaxX, stTestExtent.MaxY);
1239 
1240                 // Fill buffer transparent color.
1241                 memset( pData, 0, nBufXSize * nBufYSize * nBandCount *
1242                     GDALGetDataTypeSizeBytes(eBufType) );
1243                 return CE_None;
1244             }
1245         }
1246     }
1247     return GDALDataset::IRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData,
1248         nBufXSize, nBufYSize, eBufType, nBandCount, panBandMap, nPixelSpace,
1249         nLineSpace, nBandSpace, psExtraArg);
1250 }
1251 
1252 /*
1253  * FillCapabilities()
1254  */
FillCapabilities(char ** papszOptions)1255 void OGRNGWDataset::FillCapabilities( char **papszOptions )
1256 {
1257     // Check NGW version. Paging available from 3.1
1258     CPLJSONDocument oRouteReq;
1259     if( oRouteReq.LoadUrl( NGWAPI::GetVersion(osUrl), papszOptions ) )
1260     {
1261         CPLJSONObject oRoot = oRouteReq.GetRoot();
1262 
1263         if( oRoot.IsValid() )
1264         {
1265             std::string osVersion = oRoot.GetString("nextgisweb", "0.0");
1266             bHasFeaturePaging = NGWAPI::CheckVersion(osVersion, 3, 1);
1267 
1268             CPLDebug("NGW", "Is feature paging supported: %s",
1269                 bHasFeaturePaging ? "yes" : "no");
1270         }
1271     }
1272 }
1273 
1274 
1275 /*
1276  * Extensions()
1277  */
Extensions() const1278 std::string OGRNGWDataset::Extensions() const
1279 {
1280     return osExtensions;
1281 }
1282