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 /*
33 * GetHeaders()
34 */
GetHeaders(const std::string & osUserPwdIn="")35 static char **GetHeaders(const std::string &osUserPwdIn = "")
36 {
37 char **papszOptions = nullptr;
38 papszOptions = CSLAddString(papszOptions, "HEADERS=Accept: */*");
39 std::string osUserPwd;
40 if( osUserPwdIn.empty() )
41 {
42 osUserPwd = CPLGetConfigOption("NGW_USERPWD", "");
43 }
44 else
45 {
46 osUserPwd = osUserPwdIn;
47 }
48
49 if( !osUserPwd.empty() )
50 {
51 papszOptions = CSLAddString(papszOptions, "HTTPAUTH=BASIC");
52 std::string osUserPwdOption("USERPWD=");
53 osUserPwdOption += osUserPwd;
54 papszOptions = CSLAddString(papszOptions, osUserPwdOption.c_str());
55 }
56 return papszOptions;
57 }
58
59 /*
60 * OGRNGWDriverIdentify()
61 */
62
OGRNGWDriverIdentify(GDALOpenInfo * poOpenInfo)63 static int OGRNGWDriverIdentify( GDALOpenInfo *poOpenInfo )
64 {
65 return STARTS_WITH_CI( poOpenInfo->pszFilename, "NGW:" );
66 }
67
68 /*
69 * OGRNGWDriverOpen()
70 */
71
OGRNGWDriverOpen(GDALOpenInfo * poOpenInfo)72 static GDALDataset *OGRNGWDriverOpen( GDALOpenInfo *poOpenInfo )
73 {
74 if( OGRNGWDriverIdentify( poOpenInfo ) == 0 )
75 {
76 return nullptr;
77 }
78
79 OGRNGWDataset *poDS = new OGRNGWDataset();
80 if( !poDS->Open( poOpenInfo->pszFilename, poOpenInfo->papszOpenOptions,
81 poOpenInfo->eAccess == GA_Update, poOpenInfo->nOpenFlags ) )
82 {
83 delete poDS;
84 poDS = nullptr;
85 }
86
87 return poDS;
88 }
89
90 /*
91 * OGRNGWDriverCreate()
92 *
93 * Add new datasource name at the end of URL:
94 * NGW:http://some.nextgis.com/resource/0/new_name
95 * NGW:http://some.nextgis.com:8000/test/resource/0/new_name
96 */
97
OGRNGWDriverCreate(const char * pszName,CPL_UNUSED int nBands,CPL_UNUSED int nXSize,CPL_UNUSED int nYSize,CPL_UNUSED GDALDataType eDT,char ** papszOptions)98 static GDALDataset *OGRNGWDriverCreate( const char *pszName,
99 CPL_UNUSED int nBands,
100 CPL_UNUSED int nXSize,
101 CPL_UNUSED int nYSize,
102 CPL_UNUSED GDALDataType eDT,
103 char **papszOptions )
104
105 {
106 NGWAPI::Uri stUri = NGWAPI::ParseUri(pszName);
107 CPLErrorReset();
108 if( stUri.osPrefix != "NGW" )
109 {
110 CPLError(CE_Failure, CPLE_NotSupported, "Unsupported name %s", pszName);
111 return nullptr;
112 }
113
114 CPLDebug("NGW", "Parse uri result. URL: %s, ID: %s, New name: %s",
115 stUri.osAddress.c_str(), stUri.osResourceId.c_str(),
116 stUri.osNewResourceName.c_str());
117
118 std::string osKey = CSLFetchNameValueDef( papszOptions, "KEY", "");
119 std::string osDesc = CSLFetchNameValueDef( papszOptions, "DESCRIPTION", "");
120 std::string osUserPwd = CSLFetchNameValueDef( papszOptions, "USERPWD",
121 CPLGetConfigOption("NGW_USERPWD", "") );
122
123 CPLJSONObject oPayload;
124 CPLJSONObject oResource( "resource", oPayload );
125 oResource.Add( "cls", "resource_group" );
126 oResource.Add( "display_name", stUri.osNewResourceName );
127 if( !osKey.empty() )
128 {
129 oResource.Add( "keyname", osKey );
130 }
131
132 if( !osDesc.empty() )
133 {
134 oResource.Add( "description", osDesc );
135 }
136
137 CPLJSONObject oParent( "parent", oResource );
138 oParent.Add( "id", atoi(stUri.osResourceId.c_str()) );
139
140 std::string osNewResourceId = NGWAPI::CreateResource( stUri.osAddress,
141 oPayload.Format(CPLJSONObject::PrettyFormat::Plain), GetHeaders(osUserPwd) );
142 if( osNewResourceId == "-1" )
143 {
144 return nullptr;
145 }
146
147 OGRNGWDataset *poDS = new OGRNGWDataset();
148
149 if( !poDS->Open( stUri.osAddress, osNewResourceId, papszOptions, true, GDAL_OF_RASTER | GDAL_OF_VECTOR ) ) // TODO: GDAL_OF_GNM
150 {
151 delete poDS;
152 poDS = nullptr;
153 }
154
155 return poDS;
156 }
157
158 /*
159 * OGRNGWDriverDelete()
160 */
OGRNGWDriverDelete(const char * pszName)161 static CPLErr OGRNGWDriverDelete( const char *pszName )
162 {
163 NGWAPI::Uri stUri = NGWAPI::ParseUri(pszName);
164 CPLErrorReset();
165 if( !stUri.osNewResourceName.empty() )
166 {
167 CPLError(CE_Warning, CPLE_NotSupported, "Cannot delete new resource with name %s", pszName);
168 return CE_Failure;
169 }
170
171 if( stUri.osPrefix != "NGW" )
172 {
173 CPLError(CE_Failure, CPLE_NotSupported, "Unsupported name %s", pszName);
174 return CE_Failure;
175 }
176
177 if( stUri.osResourceId == "0" )
178 {
179 CPLError(CE_Failure, CPLE_NotSupported, "Cannot delete resource 0");
180 return CE_Failure;
181 }
182
183 char **papszOptions = GetHeaders();
184 // NGWAPI::Permissions stPermissions = NGWAPI::CheckPermissions(stUri.osAddress,
185 // stUri.osResourceId, papszOptions, true);
186 // if( stPermissions.bResourceCanDelete )
187 // {
188 return NGWAPI::DeleteResource(stUri.osAddress, stUri.osResourceId,
189 papszOptions) ? CE_None : CE_Failure;
190 // }
191 // CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
192 // return CE_Failure;
193 }
194
195 /*
196 * OGRNGWDriverRename()
197 */
OGRNGWDriverRename(const char * pszNewName,const char * pszOldName)198 static CPLErr OGRNGWDriverRename( const char *pszNewName, const char *pszOldName )
199 {
200 NGWAPI::Uri stUri = NGWAPI::ParseUri(pszOldName);
201 CPLErrorReset();
202 if( stUri.osPrefix != "NGW" )
203 {
204 CPLError(CE_Failure, CPLE_NotSupported, "Unsupported name %s", pszOldName);
205 return CE_Failure;
206 }
207 CPLDebug("NGW", "Parse uri result. URL: %s, ID: %s, New name: %s",
208 stUri.osAddress.c_str(), stUri.osResourceId.c_str(), pszNewName);
209 char **papszOptions = GetHeaders();
210 // NGWAPI::Permissions stPermissions = NGWAPI::CheckPermissions(stUri.osAddress,
211 // stUri.osResourceId, papszOptions, true);
212 // if( stPermissions.bResourceCanUpdate )
213 // {
214 return NGWAPI::RenameResource(stUri.osAddress, stUri.osResourceId,
215 pszNewName, papszOptions) ? CE_None : CE_Failure;
216 // }
217 // CPLError(CE_Failure, CPLE_AppDefined, "Operation not permitted.");
218 // return CE_Failure;
219 }
220
221 /*
222 * OGRNGWDriverCreateCopy()
223 */
OGRNGWDriverCreateCopy(const char * pszFilename,GDALDataset * poSrcDS,int bStrict,char ** papszOptions,GDALProgressFunc pfnProgress,void * pProgressData)224 static GDALDataset *OGRNGWDriverCreateCopy( const char *pszFilename,
225 GDALDataset *poSrcDS, int bStrict, char **papszOptions,
226 GDALProgressFunc pfnProgress, void *pProgressData )
227 {
228 // Check destination dataset,
229 NGWAPI::Uri stUri = NGWAPI::ParseUri(pszFilename);
230 CPLErrorReset();
231 if( stUri.osPrefix != "NGW" )
232 {
233 CPLError(CE_Failure, CPLE_NotSupported, "Unsupported name %s", pszFilename);
234 return nullptr;
235 }
236
237 // NGW v3.1 supported different raster types: 1 band and 16/32 bit, RGB/RGBA
238 // rasters and etc.
239 // For RGB/RGBA rasters we can create default raster_style.
240 // For other types - qml style file path is mandatory.
241 std::string osQMLPath = CSLFetchNameValueDef( papszOptions, "RASTER_QML_PATH", "");
242
243 // Check bands count.
244 const int nBands = poSrcDS->GetRasterCount();
245 if( nBands < 3 || nBands > 4 )
246 {
247 if( osQMLPath.empty() ) {
248 CPLError( CE_Failure, CPLE_NotSupported,
249 "Default NGW raster style supports only 3 (RGB) or 4 (RGBA). "
250 "Raster has %d bands. You must provide QML file with raster style.",
251 nBands );
252 return nullptr;
253 }
254 }
255
256 // Check band data type.
257 if( poSrcDS->GetRasterBand(1)->GetRasterDataType() != GDT_Byte )
258 {
259 if( osQMLPath.empty() ) {
260 CPLError( CE_Failure, CPLE_NotSupported,
261 "Default NGW raster style supports only 8 bit byte bands. "
262 "Raster has data type %s. You must provide QML file with raster style.",
263 GDALGetDataTypeName( poSrcDS->GetRasterBand(1)->GetRasterDataType()) );
264 return nullptr;
265 }
266 }
267
268 bool bCloseDS = false;
269 std::string osFilename;
270
271 // Check if source GDALDataset is tiff.
272 if( EQUAL(poSrcDS->GetDriverName(), "GTiff") == FALSE )
273 {
274 GDALDriver *poDriver = GetGDALDriverManager()->GetDriverByName("GTiff");
275 // Compress to minimize network transfer.
276 const char* apszOptions[] = { "COMPRESS=LZW", "NUM_THREADS=ALL_CPUS", nullptr };
277 std::string osTempFilename = CPLGenerateTempFilename("ngw_tmp");
278 osTempFilename += ".tif";
279 GDALDataset *poTmpDS = poDriver->CreateCopy( osTempFilename.c_str(),
280 poSrcDS, bStrict, const_cast<char**>(apszOptions), pfnProgress,
281 pProgressData);
282
283 if( poTmpDS != nullptr )
284 {
285 bCloseDS = true;
286 osFilename = osTempFilename;
287 poSrcDS = poTmpDS;
288 }
289 else
290 {
291 CPLError( CE_Failure, CPLE_NotSupported,
292 "NGW driver doesn't support %s source raster.",
293 poSrcDS->GetDriverName() );
294 return nullptr;
295 }
296 }
297
298 if( osFilename.empty() )
299 {
300 // Check if source tiff is local file.
301 CPLStringList oaFiles( poSrcDS->GetFileList() );
302 for( int i = 0; i < oaFiles.size(); ++i )
303 {
304 // Check extension tif
305 const char *pszExt = CPLGetExtension(oaFiles[i]);
306 if( pszExt && EQUALN(pszExt, "tif", 3) )
307 {
308 osFilename = oaFiles[i];
309 break;
310 }
311 }
312 }
313
314 if( bCloseDS )
315 {
316 GDALClose( (GDALDatasetH) poSrcDS );
317 }
318
319 std::string osKey = CSLFetchNameValueDef( papszOptions, "KEY", "");
320 std::string osDesc = CSLFetchNameValueDef( papszOptions, "DESCRIPTION", "");
321 std::string osUserPwd = CSLFetchNameValueDef( papszOptions, "USERPWD",
322 CPLGetConfigOption("NGW_USERPWD", ""));
323 std::string osStyleName = CSLFetchNameValueDef( papszOptions, "RASTER_STYLE_NAME", "");
324
325 // Send file
326 char **papszHTTPOptions = GetHeaders(osUserPwd);
327 CPLJSONObject oFileJson = NGWAPI::UploadFile(stUri.osAddress, osFilename,
328 papszHTTPOptions, pfnProgress, pProgressData);
329
330 if( bCloseDS ) // Delete temp tiff file.
331 {
332 VSIUnlink(osFilename.c_str());
333 }
334
335 if( !oFileJson.IsValid() )
336 {
337 return nullptr;
338 }
339
340 CPLJSONArray oUploadMeta = oFileJson.GetArray("upload_meta");
341 if( !oUploadMeta.IsValid() || oUploadMeta.Size() == 0 )
342 {
343 CPLError(CE_Failure, CPLE_AppDefined, "Get unexpected response: %s.",
344 oFileJson.Format(CPLJSONObject::PrettyFormat::Plain).c_str());
345 return nullptr;
346 }
347
348 // Create raster layer
349 // Create payload
350 CPLJSONObject oPayloadRaster;
351 CPLJSONObject oResource( "resource", oPayloadRaster );
352 oResource.Add( "cls", "raster_layer" );
353 oResource.Add( "display_name", stUri.osNewResourceName );
354 if( !osKey.empty() )
355 {
356 oResource.Add( "keyname", osKey );
357 }
358
359 if( !osDesc.empty() )
360 {
361 oResource.Add( "description", osDesc );
362 }
363
364 CPLJSONObject oParent( "parent", oResource );
365 oParent.Add( "id", atoi(stUri.osResourceId.c_str()) );
366
367 CPLJSONObject oRasterLayer( "raster_layer", oPayloadRaster );
368 oRasterLayer.Add( "source", oUploadMeta[0] );
369
370 CPLJSONObject oSrs("srs", oRasterLayer);
371 oSrs.Add( "id", 3857 ); // Now only Web Mercator supported.
372
373 papszHTTPOptions = GetHeaders(osUserPwd);
374 std::string osNewResourceId = NGWAPI::CreateResource( stUri.osAddress,
375 oPayloadRaster.Format(CPLJSONObject::PrettyFormat::Plain), papszHTTPOptions );
376 if( osNewResourceId == "-1" )
377 {
378 return nullptr;
379 }
380
381 // Create raster style
382 CPLJSONObject oPayloadRasterStyle;
383 CPLJSONObject oResourceStyle( "resource", oPayloadRasterStyle );
384 if( osQMLPath.empty() )
385 {
386 oResourceStyle.Add( "cls", "raster_style" );
387 }
388 else
389 {
390 oResourceStyle.Add( "cls", "qgis_raster_style" );
391
392 // Upload QML file
393 papszHTTPOptions = GetHeaders(osUserPwd);
394 oFileJson = NGWAPI::UploadFile(stUri.osAddress, osQMLPath,
395 papszHTTPOptions, pfnProgress, pProgressData);
396 oUploadMeta = oFileJson.GetArray("upload_meta");
397 if( !oUploadMeta.IsValid() || oUploadMeta.Size() == 0 )
398 {
399 CPLError(CE_Failure, CPLE_AppDefined, "Get unexpected response: %s.",
400 oFileJson.Format(CPLJSONObject::PrettyFormat::Plain).c_str());
401 return nullptr;
402 }
403 CPLJSONObject oQGISRasterStyle( "qgis_raster_style", oPayloadRasterStyle );
404 oQGISRasterStyle.Add( "file_upload", oUploadMeta[0]);
405 }
406
407 if( osStyleName.empty() )
408 {
409 osStyleName = stUri.osNewResourceName;
410 }
411 oResourceStyle.Add( "display_name", osStyleName );
412 CPLJSONObject oParentRaster( "parent", oResourceStyle );
413 oParentRaster.Add( "id", atoi(osNewResourceId.c_str()) );
414
415 papszHTTPOptions = GetHeaders(osUserPwd);
416 osNewResourceId = NGWAPI::CreateResource( stUri.osAddress,
417 oPayloadRasterStyle.Format(CPLJSONObject::PrettyFormat::Plain), papszHTTPOptions );
418 if( osNewResourceId == "-1" )
419 {
420 return nullptr;
421 }
422
423 OGRNGWDataset *poDS = new OGRNGWDataset();
424
425 if( !poDS->Open( stUri.osAddress, osNewResourceId, papszOptions, true, GDAL_OF_RASTER ) )
426 {
427 delete poDS;
428 poDS = nullptr;
429 }
430
431 return poDS;
432 }
433
434 /*
435 * RegisterOGRNGW()
436 */
437
RegisterOGRNGW()438 void RegisterOGRNGW()
439 {
440 if( GDALGetDriverByName( "NGW" ) != nullptr )
441 {
442 return;
443 }
444
445 GDALDriver *poDriver = new GDALDriver();
446
447 poDriver->SetDescription( "NGW" );
448 poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "NextGIS Web" );
449 poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
450 poDriver->SetMetadataItem( GDAL_DCAP_VECTOR, "YES" );
451 poDriver->SetMetadataItem( GDAL_DMD_SUBDATASETS, "YES" );
452 poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/vector/ngw.html" );
453 poDriver->SetMetadataItem( GDAL_DMD_CONNECTION_PREFIX, "NGW:" );
454
455 poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES, "Byte" );
456 poDriver->SetMetadataItem( GDAL_DCAP_CREATECOPY, "YES" );
457
458 poDriver->SetMetadataItem( GDAL_DMD_OPENOPTIONLIST,
459 "<OpenOptionList>"
460 " <Option name='USERPWD' scope='raster,vector' type='string' description='Username and password, separated by colon'/>"
461 " <Option name='PAGE_SIZE' scope='vector' type='integer' description='Limit feature count while fetching from server. Default value is -1 - no limit' default='-1'/>"
462 " <Option name='BATCH_SIZE' scope='vector' type='integer' description='Size of feature insert and update operations cache before send to server. If batch size is -1 batch mode is disabled' default='-1'/>"
463 " <Option name='NATIVE_DATA' scope='vector' type='boolean' description='Whether to store the native Json representation of extensions key. If EXTENSIONS not set or empty, NATIVE_DATA defaults to NO' default='NO'/>"
464 " <Option name='CACHE_EXPIRES' scope='raster' type='integer' description='Time in seconds cached files will stay valid. If cached file expires it is deleted when maximum size of cache is reached. Also expired file can be overwritten by the new one from web' default='604800'/>"
465 " <Option name='CACHE_MAX_SIZE' scope='raster' type='integer' description='The cache maximum size in bytes. If cache reached maximum size, expired cached files will be deleted' default='67108864'/>"
466 " <Option name='JSON_DEPTH' scope='raster,vector' type='integer' description='The depth of json response that can be parsed. If depth is greater than this value, parse error occurs' default='32'/>"
467 " <Option name='EXTENSIONS' scope='vector' type='string' description='Comma separated extensions list. Available are description and attachment' default=''/>"
468 "</OpenOptionList>"
469 );
470
471 poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
472 "<CreationOptionList>"
473 " <Option name='KEY' scope='raster,vector' type='string' description='Key value. Must be unique in whole NextGIS Web instance'/>"
474 " <Option name='DESCRIPTION' scope='raster,vector' type='string' description='Resource description'/>"
475 " <Option name='RASTER_STYLE_NAME' scope='raster' type='string' description='Raster layer style name'/>"
476 " <Option name='USERPWD' scope='raster,vector' type='string' description='Username and password, separated by colon'/>"
477 " <Option name='PAGE_SIZE' scope='vector' type='integer' description='Limit feature count while fetching from server. Default value is -1 - no limit' default='-1'/>"
478 " <Option name='BATCH_SIZE' scope='vector' type='integer' description='Size of feature insert and update operations cache before send to server. If batch size is -1 batch mode is disabled' default='-1'/>"
479 " <Option name='NATIVE_DATA' scope='vector' type='boolean' description='Whether to store the native Json representation of extensions key. If EXTENSIONS not set or empty, NATIVE_DATA defaults to NO' default='NO'/>"
480 " <Option name='CACHE_EXPIRES' scope='raster' type='integer' description='Time in seconds cached files will stay valid. If cached file expires it is deleted when maximum size of cache is reached. Also expired file can be overwritten by the new one from web' default='604800'/>"
481 " <Option name='CACHE_MAX_SIZE' scope='raster' type='integer' description='The cache maximum size in bytes. If cache reached maximum size, expired cached files will be deleted' default='67108864'/>"
482 " <Option name='JSON_DEPTH' scope='raster,vector' type='integer' description='The depth of json response that can be parsed. If depth is greater than this value, parse error occurs' default='32'/>"
483 " <Option name='RASTER_QML_PATH' scope='raster' type='string' description='Raster QMS style path'/>"
484 " <Option name='EXTENSIONS' scope='vector' type='string' description='Comma separated extensions list. Available are description and attachment' default=''/>"
485 "</CreationOptionList>"
486 );
487
488 poDriver->SetMetadataItem( GDAL_DS_LAYER_CREATIONOPTIONLIST,
489 "<LayerCreationOptionList>"
490 " <Option name='OVERWRITE' type='boolean' description='Whether to overwrite an existing table with the layer name to be created' default='NO'/>"
491 " <Option name='KEY' type='string' description='Key value. Must be unique in whole NextGIS Web instance'/>"
492 " <Option name='DESCRIPTION' type='string' description='Resource description'/>"
493 "</LayerCreationOptionList>"
494 );
495
496 poDriver->SetMetadataItem( GDAL_DMD_CREATIONFIELDDATATYPES, "Integer Integer64 Real String Date DateTime Time" );
497 poDriver->SetMetadataItem( GDAL_DCAP_NOTNULL_FIELDS, "NO" );
498 poDriver->SetMetadataItem( GDAL_DCAP_DEFAULT_FIELDS, "NO" );
499 poDriver->SetMetadataItem( GDAL_DCAP_NOTNULL_GEOMFIELDS, "YES" );
500
501 poDriver->pfnOpen = OGRNGWDriverOpen;
502 poDriver->pfnIdentify = OGRNGWDriverIdentify;
503 poDriver->pfnCreate = OGRNGWDriverCreate;
504 poDriver->pfnCreateCopy = OGRNGWDriverCreateCopy;
505 poDriver->pfnDelete = OGRNGWDriverDelete;
506 poDriver->pfnRename = OGRNGWDriverRename;
507
508 GetGDALDriverManager()->RegisterDriver( poDriver );
509 }
510