1 /*************************************************************************** 2 qgswfsgetfeature.cpp 3 ------------------------- 4 begin : December 20 , 2016 5 copyright : (C) 2007 by Marco Hugentobler (original code) 6 (C) 2012 by René-Luc D'Hont (original code) 7 (C) 2014 by Alessandro Pasotti (original code) 8 (C) 2017 by David Marteau 9 email : marco dot hugentobler at karto dot baug dot ethz dot ch 10 a dot pasotti at itopen dot it 11 david dot marteau at 3liz dot com 12 ***************************************************************************/ 13 14 /*************************************************************************** 15 * * 16 * This program is free software; you can redistribute it and/or modify * 17 * it under the terms of the GNU General Public License as published by * 18 * the Free Software Foundation; either version 2 of the License, or * 19 * (at your option) any later version. * 20 * * 21 ***************************************************************************/ 22 #include "qgswfsutils.h" 23 #include "qgsserverprojectutils.h" 24 #include "qgsserverfeatureid.h" 25 #include "qgsfields.h" 26 #include "qgsdatetimefieldformatter.h" 27 #include "qgsexpression.h" 28 #include "qgsgeometry.h" 29 #include "qgsmaplayer.h" 30 #include "qgsfeatureiterator.h" 31 #include "qgscoordinatereferencesystem.h" 32 #include "qgsvectorlayer.h" 33 #include "qgsfilterrestorer.h" 34 #include "qgsproject.h" 35 #include "qgsogcutils.h" 36 #include "qgsjsonutils.h" 37 #include "qgsexpressioncontextutils.h" 38 #include "qgswkbtypes.h" 39 40 #include "qgswfsgetfeature.h" 41 42 namespace QgsWfs 43 { 44 45 namespace 46 { 47 struct createFeatureParams 48 { 49 int precision; 50 51 const QgsCoordinateReferenceSystem &crs; 52 53 const QgsAttributeList &attributeIndexes; 54 55 const QString &typeName; 56 57 bool withGeom; 58 59 const QString &geometryName; 60 61 const QgsCoordinateReferenceSystem &outputCrs; 62 63 bool forceGeomToMulti; 64 }; 65 66 QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams ¶ms, const QgsAttributeList &pkAttributes ); 67 68 QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup ); 69 70 QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ); 71 72 QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ); 73 74 void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, 75 QgsWfsParameters::Format format, int numberOfFeatures, const QStringList &typeNames ); 76 77 void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, 78 QgsWfsParameters::Format format, int prec, QgsCoordinateReferenceSystem &crs, 79 QgsRectangle *rect, const QStringList &typeNames ); 80 81 void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx, 82 const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes = QgsAttributeList() ); 83 84 void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format ); 85 86 QgsServerRequest::Parameters mRequestParameters; 87 QgsWfsParameters mWfsParameters; 88 /* GeoJSON Exporter */ 89 QgsJsonExporter mJsonExporter; 90 } 91 writeGetFeature(QgsServerInterface * serverIface,const QgsProject * project,const QString & version,const QgsServerRequest & request,QgsServerResponse & response)92 void writeGetFeature( QgsServerInterface *serverIface, const QgsProject *project, 93 const QString &version, const QgsServerRequest &request, 94 QgsServerResponse &response ) 95 { 96 Q_UNUSED( version ) 97 98 mRequestParameters = request.parameters(); 99 mWfsParameters = QgsWfsParameters( QUrlQuery( request.url() ) ); 100 mWfsParameters.dump(); 101 getFeatureRequest aRequest; 102 103 QDomDocument doc; 104 QString errorMsg; 105 106 if ( doc.setContent( request.data(), true, &errorMsg ) ) 107 { 108 QDomElement docElem = doc.documentElement(); 109 aRequest = parseGetFeatureRequestBody( docElem, project ); 110 } 111 else 112 { 113 aRequest = parseGetFeatureParameters( project ); 114 } 115 116 // store typeName 117 QStringList typeNameList; 118 119 // Request metadata 120 bool onlyOneLayer = ( aRequest.queries.size() == 1 ); 121 QgsRectangle requestRect; 122 QgsCoordinateReferenceSystem requestCrs; 123 int requestPrecision = 6; 124 if ( !onlyOneLayer ) 125 requestCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); 126 127 QList<getFeatureQuery>::iterator qIt = aRequest.queries.begin(); 128 for ( ; qIt != aRequest.queries.end(); ++qIt ) 129 { 130 typeNameList << ( *qIt ).typeName; 131 } 132 133 // get layers and 134 // update the request metadata 135 QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project ); 136 QMap<QString, QgsMapLayer *> mapLayerMap; 137 for ( int i = 0; i < wfsLayerIds.size(); ++i ) 138 { 139 QgsMapLayer *layer = project->mapLayer( wfsLayerIds.at( i ) ); 140 if ( !layer ) 141 { 142 continue; 143 } 144 if ( layer->type() != QgsMapLayerType::VectorLayer ) 145 { 146 continue; 147 } 148 149 QString name = layerTypeName( layer ); 150 151 if ( typeNameList.contains( name ) ) 152 { 153 // store layers 154 mapLayerMap[name] = layer; 155 // update request metadata 156 if ( onlyOneLayer ) 157 { 158 requestRect = layer->extent(); 159 requestCrs = layer->crs(); 160 } 161 else 162 { 163 QgsCoordinateTransform transform( layer->crs(), requestCrs, project ); 164 try 165 { 166 if ( requestRect.isEmpty() ) 167 { 168 requestRect = transform.transform( layer->extent() ); 169 } 170 else 171 { 172 requestRect.combineExtentWith( transform.transform( layer->extent() ) ); 173 } 174 } 175 catch ( QgsException &cse ) 176 { 177 Q_UNUSED( cse ) 178 requestRect = QgsRectangle( -180.0, -90.0, 180.0, 90.0 ); 179 } 180 } 181 } 182 } 183 184 #ifdef HAVE_SERVER_PYTHON_PLUGINS 185 QgsAccessControl *accessControl = serverIface->accessControls(); 186 //scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope 187 //there's LOTS of potential exit paths here, so we avoid having to restore the filters manually 188 std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() ); 189 #else 190 ( void )serverIface; 191 #endif 192 193 // features counters 194 long sentFeatures = 0; 195 long iteratedFeatures = 0; 196 // sent features 197 QgsFeature feature; 198 qIt = aRequest.queries.begin(); 199 for ( ; qIt != aRequest.queries.end(); ++qIt ) 200 { 201 getFeatureQuery &query = *qIt; 202 QString typeName = query.typeName; 203 204 if ( !mapLayerMap.contains( typeName ) ) 205 { 206 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' unknown" ).arg( typeName ) ); 207 } 208 209 QgsMapLayer *layer = mapLayerMap[typeName]; 210 #ifdef HAVE_SERVER_PYTHON_PLUGINS 211 if ( accessControl && !accessControl->layerReadPermission( layer ) ) 212 { 213 throw QgsSecurityAccessException( QStringLiteral( "Feature access permission denied" ) ); 214 } 215 #endif 216 QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer ); 217 if ( !vlayer ) 218 { 219 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer error" ).arg( typeName ) ); 220 } 221 222 //test provider 223 QgsVectorDataProvider *provider = vlayer->dataProvider(); 224 if ( !provider ) 225 { 226 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer's provider error" ).arg( typeName ) ); 227 } 228 #ifdef HAVE_SERVER_PYTHON_PLUGINS 229 if ( accessControl ) 230 { 231 QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, vlayer, filterRestorer->originalFilters() ); 232 } 233 #endif 234 //is there alias info for this vector layer? 235 QMap< int, QString > layerAliasInfo; 236 QgsStringMap aliasMap = vlayer->attributeAliases(); 237 QgsStringMap::const_iterator aliasIt = aliasMap.constBegin(); 238 for ( ; aliasIt != aliasMap.constEnd(); ++aliasIt ) 239 { 240 int attrIndex = vlayer->fields().lookupField( aliasIt.key() ); 241 if ( attrIndex != -1 ) 242 { 243 layerAliasInfo.insert( attrIndex, aliasIt.value() ); 244 } 245 } 246 247 // get propertyList from query 248 const QStringList propertyList = query.propertyList; 249 250 //Using pending attributes and pending fields 251 QgsAttributeList attrIndexes = vlayer->attributeList(); 252 const QgsFields fields = vlayer->fields(); 253 bool withGeom = true; 254 if ( !propertyList.isEmpty() && propertyList.first() != QLatin1String( "*" ) ) 255 { 256 withGeom = false; 257 QStringList::const_iterator plstIt; 258 QList<int> idxList; 259 // build corresponding propertyname 260 QList<QString> propertynames; 261 QList<QString> fieldnames; 262 for ( const QgsField &field : fields ) 263 { 264 fieldnames.append( field.name() ); 265 propertynames.append( field.name().replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 266 } 267 QString fieldName; 268 for ( plstIt = propertyList.constBegin(); plstIt != propertyList.constEnd(); ++plstIt ) 269 { 270 fieldName = *plstIt; 271 int fieldNameIdx = propertynames.indexOf( fieldName ); 272 if ( fieldNameIdx == -1 ) 273 { 274 fieldNameIdx = fieldnames.indexOf( fieldName ); 275 } 276 if ( fieldNameIdx > -1 ) 277 { 278 idxList.append( fieldNameIdx ); 279 } 280 else if ( fieldName == QLatin1String( "geometry" ) ) 281 { 282 withGeom = true; 283 } 284 } 285 if ( !idxList.isEmpty() ) 286 { 287 attrIndexes = idxList; 288 } 289 } 290 291 //excluded attributes for this layer 292 if ( !attrIndexes.isEmpty() ) 293 { 294 for ( const QgsField &field : fields ) 295 { 296 if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::HideFromWfs ) ) 297 { 298 int fieldNameIdx = fields.indexOf( field.name() ); 299 if ( fieldNameIdx > -1 && attrIndexes.contains( fieldNameIdx ) ) 300 { 301 attrIndexes.removeOne( fieldNameIdx ); 302 } 303 } 304 } 305 } 306 307 // update request 308 QgsFeatureRequest featureRequest = query.featureRequest; 309 310 // expression context 311 QgsExpressionContext expressionContext; 312 expressionContext << QgsExpressionContextUtils::globalScope() 313 << QgsExpressionContextUtils::projectScope( project ) 314 << QgsExpressionContextUtils::layerScope( vlayer ); 315 featureRequest.setExpressionContext( expressionContext ); 316 317 if ( !query.serverFids.isEmpty() ) 318 { 319 QgsServerFeatureId::updateFeatureRequestFromServerFids( featureRequest, query.serverFids, provider ); 320 } 321 322 // geometry flags 323 if ( vlayer->wkbType() == QgsWkbTypes::NoGeometry ) 324 featureRequest.setFlags( featureRequest.flags() | QgsFeatureRequest::NoGeometry ); 325 else 326 featureRequest.setFlags( featureRequest.flags() | ( withGeom ? QgsFeatureRequest::NoFlags : QgsFeatureRequest::NoGeometry ) ); 327 328 // subset of attributes 329 featureRequest.setSubsetOfAttributes( attrIndexes ); 330 #ifdef HAVE_SERVER_PYTHON_PLUGINS 331 if ( accessControl ) 332 { 333 accessControl->filterFeatures( vlayer, featureRequest ); 334 335 QStringList attributes = QStringList(); 336 for ( int idx : qgis::as_const( attrIndexes ) ) 337 { 338 attributes.append( vlayer->fields().field( idx ).name() ); 339 } 340 featureRequest.setSubsetOfAttributes( 341 accessControl->layerAttributes( vlayer, attributes ), 342 vlayer->fields() ); 343 attrIndexes = featureRequest.subsetOfAttributes(); 344 } 345 #endif 346 347 // Force pkAttributes in subset of attributes for primary fid building 348 const QgsAttributeList pkAttributes = provider->pkAttributeIndexes(); 349 if ( !pkAttributes.isEmpty() ) 350 { 351 QgsAttributeList subsetOfAttrs = featureRequest.subsetOfAttributes(); 352 for ( int idx : pkAttributes ) 353 { 354 if ( !subsetOfAttrs.contains( idx ) ) 355 { 356 subsetOfAttrs.prepend( idx ); 357 } 358 } 359 if ( subsetOfAttrs.size() != featureRequest.subsetOfAttributes().size() ) 360 { 361 featureRequest.setSubsetOfAttributes( subsetOfAttrs ); 362 } 363 } 364 365 if ( onlyOneLayer ) 366 { 367 requestPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() ); 368 } 369 370 if ( aRequest.maxFeatures > 0 ) 371 { 372 featureRequest.setLimit( aRequest.maxFeatures + aRequest.startIndex - sentFeatures ); 373 } 374 // specific layer precision 375 int layerPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() ); 376 // specific layer crs 377 QgsCoordinateReferenceSystem layerCrs = vlayer->crs(); 378 379 // Geometry name 380 QString geometryName = aRequest.geometryName; 381 if ( !withGeom ) 382 { 383 geometryName = QLatin1String( "NONE" ); 384 } 385 // outputCrs 386 QgsCoordinateReferenceSystem outputCrs = vlayer->crs(); 387 if ( !query.srsName.isEmpty() ) 388 { 389 outputCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( query.srsName ); 390 } 391 392 bool forceGeomToMulti = QgsWkbTypes::isMultiType( vlayer->wkbType() ); 393 394 if ( !featureRequest.filterRect().isEmpty() ) 395 { 396 QgsCoordinateTransform transform( outputCrs, vlayer->crs(), project ); 397 try 398 { 399 featureRequest.setFilterRect( transform.transform( featureRequest.filterRect() ) ); 400 } 401 catch ( QgsException &cse ) 402 { 403 Q_UNUSED( cse ) 404 } 405 if ( onlyOneLayer ) 406 { 407 requestRect = featureRequest.filterRect(); 408 } 409 } 410 411 // Iterate through features 412 QgsFeatureIterator fit = vlayer->getFeatures( featureRequest ); 413 414 if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS ) 415 { 416 while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) ) 417 { 418 if ( iteratedFeatures >= aRequest.startIndex ) 419 { 420 ++sentFeatures; 421 } 422 ++iteratedFeatures; 423 } 424 } 425 else 426 { 427 const createFeatureParams cfp = { layerPrecision, 428 layerCrs, 429 attrIndexes, 430 typeName, 431 withGeom, 432 geometryName, 433 outputCrs, 434 forceGeomToMulti 435 }; 436 while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) ) 437 { 438 if ( iteratedFeatures == aRequest.startIndex ) 439 startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList ); 440 441 if ( iteratedFeatures >= aRequest.startIndex ) 442 { 443 setGetFeature( response, aRequest.outputFormat, feature, sentFeatures, cfp, project, provider->pkAttributeIndexes() ); 444 ++sentFeatures; 445 } 446 ++iteratedFeatures; 447 } 448 } 449 } 450 451 #ifdef HAVE_SERVER_PYTHON_PLUGINS 452 //force restoration of original layer filters 453 filterRestorer.reset(); 454 #endif 455 456 if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS ) 457 { 458 hitGetFeature( request, response, project, aRequest.outputFormat, sentFeatures, typeNameList ); 459 } 460 else 461 { 462 // End of GetFeature 463 if ( iteratedFeatures <= aRequest.startIndex ) 464 startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList ); 465 endGetFeature( response, aRequest.outputFormat ); 466 } 467 468 } 469 parseGetFeatureParameters(const QgsProject * project)470 getFeatureRequest parseGetFeatureParameters( const QgsProject *project ) 471 { 472 getFeatureRequest request; 473 request.maxFeatures = mWfsParameters.maxFeaturesAsInt(); 474 request.startIndex = mWfsParameters.startIndexAsInt(); 475 request.outputFormat = mWfsParameters.outputFormat(); 476 477 // Verifying parameters mutually exclusive 478 QStringList fidList = mWfsParameters.featureIds(); 479 bool paramContainsFeatureIds = !fidList.isEmpty(); 480 QStringList filterList = mWfsParameters.filters(); 481 bool paramContainsFilters = !filterList.isEmpty(); 482 QString bbox = mWfsParameters.bbox(); 483 bool paramContainsBbox = !bbox.isEmpty(); 484 if ( ( paramContainsFeatureIds 485 && ( paramContainsFilters || paramContainsBbox ) ) 486 || ( paramContainsFilters 487 && ( paramContainsFeatureIds || paramContainsBbox ) ) 488 || ( paramContainsBbox 489 && ( paramContainsFeatureIds || paramContainsFilters ) ) 490 ) 491 { 492 throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID FILTER and BBOX parameters are mutually exclusive" ) ); 493 } 494 495 // Get and split PROPERTYNAME parameter 496 QStringList propertyNameList = mWfsParameters.propertyNames(); 497 498 // Manage extra parameter GeometryName 499 request.geometryName = mWfsParameters.geometryNameAsString().toUpper(); 500 501 QStringList typeNameList; 502 // parse FEATUREID 503 if ( paramContainsFeatureIds ) 504 { 505 // Verifying the 1:1 mapping between FEATUREID and PROPERTYNAME 506 if ( !propertyNameList.isEmpty() && propertyNameList.size() != fidList.size() ) 507 { 508 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a FEATUREID and the PROPERTYNAME list" ) ); 509 } 510 if ( propertyNameList.isEmpty() ) 511 { 512 for ( int i = 0; i < fidList.size(); ++i ) 513 { 514 propertyNameList << QStringLiteral( "*" ); 515 } 516 } 517 518 QMap<QString, QStringList> fidsMap; 519 520 QStringList::const_iterator fidIt = fidList.constBegin(); 521 QStringList::const_iterator propertyNameIt = propertyNameList.constBegin(); 522 for ( ; fidIt != fidList.constEnd(); ++fidIt ) 523 { 524 // Get FeatureID 525 QString fid = *fidIt; 526 fid = fid.trimmed(); 527 // Get PropertyName for this FeatureID 528 QString propertyName; 529 if ( propertyNameIt != propertyNameList.constEnd() ) 530 { 531 propertyName = *propertyNameIt; 532 } 533 // testing typename in the WFS featureID 534 if ( !fid.contains( '.' ) ) 535 { 536 throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID has to have TYPENAME in the values" ) ); 537 } 538 539 QString typeName = fid.section( '.', 0, 0 ); 540 fid = fid.section( '.', 1, 1 ); 541 if ( !typeNameList.contains( typeName ) ) 542 { 543 typeNameList << typeName; 544 } 545 546 // each Feature requested by FEATUREID can have each own property list 547 QString key = QStringLiteral( "%1(%2)" ).arg( typeName, propertyName ); 548 QStringList fids; 549 if ( fidsMap.contains( key ) ) 550 { 551 fids = fidsMap.value( key ); 552 } 553 fids.append( fid ); 554 fidsMap.insert( key, fids ); 555 556 if ( propertyNameIt != propertyNameList.constEnd() ) 557 { 558 ++propertyNameIt; 559 } 560 } 561 562 QMap<QString, QStringList>::const_iterator fidsMapIt = fidsMap.constBegin(); 563 while ( fidsMapIt != fidsMap.constEnd() ) 564 { 565 QString key = fidsMapIt.key(); 566 567 //Extract TypeName and PropertyName from key 568 QRegExp rx( "([^()]+)\\(([^()]+)\\)" ); 569 if ( rx.indexIn( key, 0 ) == -1 ) 570 { 571 throw QgsRequestNotWellFormedException( QStringLiteral( "Error getting properties for FEATUREID" ) ); 572 } 573 QString typeName = rx.cap( 1 ); 574 QString propertyName = rx.cap( 2 ); 575 576 getFeatureQuery query; 577 query.typeName = typeName; 578 query.srsName = mWfsParameters.srsName(); 579 580 // Parse PropertyName 581 if ( propertyName != QLatin1String( "*" ) ) 582 { 583 QStringList propertyList; 584 585 const QStringList attrList = propertyName.split( ',' ); 586 QStringList::const_iterator alstIt; 587 for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt ) 588 { 589 QString fieldName = *alstIt; 590 fieldName = fieldName.trimmed(); 591 if ( fieldName.contains( ':' ) ) 592 { 593 fieldName = fieldName.section( ':', 1, 1 ); 594 } 595 if ( fieldName.contains( '/' ) ) 596 { 597 if ( fieldName.section( '/', 0, 0 ) != typeName ) 598 { 599 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 600 } 601 fieldName = fieldName.section( '/', 1, 1 ); 602 } 603 propertyList.append( fieldName ); 604 } 605 query.propertyList = propertyList; 606 } 607 608 query.serverFids = fidsMapIt.value(); 609 QgsFeatureRequest featureRequest; 610 611 query.featureRequest = featureRequest; 612 request.queries.append( query ); 613 ++fidsMapIt; 614 } 615 return request; 616 } 617 618 if ( !mRequestParameters.contains( QStringLiteral( "TYPENAME" ) ) ) 619 { 620 throw QgsRequestNotWellFormedException( QStringLiteral( "TYPENAME is mandatory except if FEATUREID is used" ) ); 621 } 622 623 typeNameList = mWfsParameters.typeNames(); 624 // Verifying the 1:1 mapping between TYPENAME and PROPERTYNAME 625 if ( !propertyNameList.isEmpty() && typeNameList.size() != propertyNameList.size() ) 626 { 627 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the PROPERTYNAME list" ) ); 628 } 629 if ( propertyNameList.isEmpty() ) 630 { 631 for ( int i = 0; i < typeNameList.size(); ++i ) 632 { 633 propertyNameList << QStringLiteral( "*" ); 634 } 635 } 636 637 // Create queries based on TypeName and propertyName 638 QStringList::const_iterator typeNameIt = typeNameList.constBegin(); 639 QStringList::const_iterator propertyNameIt = propertyNameList.constBegin(); 640 for ( ; typeNameIt != typeNameList.constEnd(); ++typeNameIt ) 641 { 642 QString typeName = *typeNameIt; 643 typeName = typeName.trimmed(); 644 // Get PropertyName for this typeName 645 QString propertyName; 646 if ( propertyNameIt != propertyNameList.constEnd() ) 647 { 648 propertyName = *propertyNameIt; 649 } 650 651 getFeatureQuery query; 652 query.typeName = typeName; 653 query.srsName = mWfsParameters.srsName(); 654 655 // Parse PropertyName 656 if ( propertyName != QLatin1String( "*" ) ) 657 { 658 QStringList propertyList; 659 660 const QStringList attrList = propertyName.split( ',' ); 661 QStringList::const_iterator alstIt; 662 for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt ) 663 { 664 QString fieldName = *alstIt; 665 fieldName = fieldName.trimmed(); 666 if ( fieldName.contains( ':' ) ) 667 { 668 fieldName = fieldName.section( ':', 1, 1 ); 669 } 670 if ( fieldName.contains( '/' ) ) 671 { 672 if ( fieldName.section( '/', 0, 0 ) != typeName ) 673 { 674 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 675 } 676 fieldName = fieldName.section( '/', 1, 1 ); 677 } 678 propertyList.append( fieldName ); 679 } 680 query.propertyList = propertyList; 681 } 682 683 request.queries.append( query ); 684 685 if ( propertyNameIt != propertyNameList.constEnd() ) 686 { 687 ++propertyNameIt; 688 } 689 } 690 691 // Manage extra parameter exp_filter 692 QStringList expFilterList = mWfsParameters.expFilters(); 693 if ( !expFilterList.isEmpty() ) 694 { 695 // Verifying the 1:1 mapping between TYPENAME and EXP_FILTER but without exception 696 if ( request.queries.size() == expFilterList.size() ) 697 { 698 // set feature request filter expression based on filter element 699 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 700 QStringList::const_iterator expFilterIt = expFilterList.constBegin(); 701 for ( ; qIt != request.queries.end(); ++qIt ) 702 { 703 getFeatureQuery &query = *qIt; 704 // Get Filter for this typeName 705 QString expFilter; 706 if ( expFilterIt != expFilterList.constEnd() ) 707 { 708 expFilter = *expFilterIt; 709 } 710 std::shared_ptr<QgsExpression> filter( new QgsExpression( expFilter ) ); 711 if ( filter ) 712 { 713 if ( filter->hasParserError() ) 714 { 715 throw QgsRequestNotWellFormedException( QStringLiteral( "The EXP_FILTER expression has errors: %1" ).arg( filter->parserErrorString() ) ); 716 } 717 if ( filter->needsGeometry() ) 718 { 719 query.featureRequest.setFlags( QgsFeatureRequest::NoFlags ); 720 } 721 query.featureRequest.setFilterExpression( filter->expression() ); 722 } 723 } 724 } 725 else 726 { 727 QgsMessageLog::logMessage( "There has to be a 1:1 mapping between each element in a TYPENAME and the EXP_FILTER list" ); 728 } 729 } 730 731 if ( paramContainsBbox ) 732 { 733 734 // get bbox extent 735 QgsRectangle extent = mWfsParameters.bboxAsRectangle(); 736 737 QString extentSrsName { mWfsParameters.srsName() }; 738 739 // handle WFS 1.1.0 optional CRS 740 if ( mWfsParameters.bbox().split( ',' ).size() == 5 && ! mWfsParameters.srsName().isEmpty() ) 741 { 742 QString crs( mWfsParameters.bbox().split( ',' )[4] ); 743 if ( crs != mWfsParameters.srsName() ) 744 { 745 extentSrsName = crs; 746 QgsCoordinateReferenceSystem sourceCrs( crs ); 747 QgsCoordinateReferenceSystem destinationCrs( mWfsParameters.srsName() ); 748 if ( sourceCrs.isValid() && destinationCrs.isValid( ) ) 749 { 750 QgsGeometry extentGeom = QgsGeometry::fromRect( extent ); 751 QgsCoordinateTransform transform; 752 transform.setSourceCrs( sourceCrs ); 753 transform.setDestinationCrs( destinationCrs ); 754 try 755 { 756 if ( extentGeom.transform( transform ) == 0 ) 757 { 758 extent = QgsRectangle( extentGeom.boundingBox() ); 759 } 760 } 761 catch ( QgsException &cse ) 762 { 763 Q_UNUSED( cse ) 764 } 765 } 766 } 767 } 768 769 // Follow GeoServer conventions and handle axis order 770 // See: https://docs.geoserver.org/latest/en/user/services/wfs/axis_order.html#wfs-basics-axis 771 QgsCoordinateReferenceSystem extentCrs; 772 extentCrs.createFromUserInput( extentSrsName ); 773 if ( extentCrs.isValid() && extentCrs.hasAxisInverted() && ! extentSrsName.startsWith( QStringLiteral( "EPSG:" ) ) ) 774 { 775 QgsGeometry geom { QgsGeometry::fromRect( extent ) }; 776 geom.get()->swapXy(); 777 extent = geom.boundingBox(); 778 } 779 780 // set feature request filter rectangle 781 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 782 for ( ; qIt != request.queries.end(); ++qIt ) 783 { 784 getFeatureQuery &query = *qIt; 785 query.featureRequest.setFilterRect( extent ).setFlags( query.featureRequest.flags() | QgsFeatureRequest::ExactIntersect ); 786 } 787 return request; 788 } 789 else if ( paramContainsFilters ) 790 { 791 // Verifying the 1:1 mapping between TYPENAME and FILTER 792 if ( request.queries.size() != filterList.size() ) 793 { 794 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the FILTER list" ) ); 795 } 796 797 // set feature request filter expression based on filter element 798 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 799 QStringList::const_iterator filterIt = filterList.constBegin(); 800 for ( ; qIt != request.queries.end(); ++qIt ) 801 { 802 getFeatureQuery &query = *qIt; 803 // Get Filter for this typeName 804 QDomDocument filter; 805 if ( filterIt != filterList.constEnd() ) 806 { 807 QString errorMsg; 808 if ( !filter.setContent( *filterIt, true, &errorMsg ) ) 809 { 810 throw QgsRequestNotWellFormedException( QStringLiteral( "error message: %1. The XML string was: %2" ).arg( errorMsg, *filterIt ) ); 811 } 812 } 813 814 QDomElement filterElem = filter.firstChildElement(); 815 QStringList serverFids; 816 query.featureRequest = parseFilterElement( query.typeName, filterElem, serverFids, project ); 817 query.serverFids = serverFids; 818 819 if ( filterIt != filterList.constEnd() ) 820 { 821 ++filterIt; 822 } 823 } 824 return request; 825 } 826 827 QStringList sortByList = mWfsParameters.sortBy(); 828 if ( !sortByList.isEmpty() && request.queries.size() == sortByList.size() ) 829 { 830 // add order by to feature request 831 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 832 QStringList::const_iterator sortByIt = sortByList.constBegin(); 833 for ( ; qIt != request.queries.end(); ++qIt ) 834 { 835 getFeatureQuery &query = *qIt; 836 // Get sortBy for this typeName 837 QString sortBy; 838 if ( sortByIt != sortByList.constEnd() ) 839 { 840 sortBy = *sortByIt; 841 } 842 for ( const QString &attribute : sortBy.split( ',' ) ) 843 { 844 if ( attribute.endsWith( QLatin1String( " D" ) ) || attribute.endsWith( QLatin1String( "+D" ) ) ) 845 { 846 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ), false ); 847 } 848 else if ( attribute.endsWith( QLatin1String( " DESC" ) ) || attribute.endsWith( QLatin1String( "+DESC" ) ) ) 849 { 850 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 5 ), false ); 851 } 852 else if ( attribute.endsWith( QLatin1String( " A" ) ) || attribute.endsWith( QLatin1String( "+A" ) ) ) 853 { 854 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ) ); 855 } 856 else if ( attribute.endsWith( QLatin1String( " ASC" ) ) || attribute.endsWith( QLatin1String( "+ASC" ) ) ) 857 { 858 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 4 ) ); 859 } 860 else 861 { 862 query.featureRequest.addOrderBy( attribute ); 863 } 864 } 865 } 866 } 867 868 return request; 869 } 870 parseGetFeatureRequestBody(QDomElement & docElem,const QgsProject * project)871 getFeatureRequest parseGetFeatureRequestBody( QDomElement &docElem, const QgsProject *project ) 872 { 873 getFeatureRequest request; 874 request.maxFeatures = mWfsParameters.maxFeaturesAsInt(); 875 request.startIndex = mWfsParameters.startIndexAsInt(); 876 request.outputFormat = mWfsParameters.outputFormat(); 877 878 QDomNodeList queryNodes = docElem.elementsByTagName( QStringLiteral( "Query" ) ); 879 QDomElement queryElem; 880 for ( int i = 0; i < queryNodes.size(); i++ ) 881 { 882 queryElem = queryNodes.at( i ).toElement(); 883 getFeatureQuery query = parseQueryElement( queryElem, project ); 884 request.queries.append( query ); 885 } 886 return request; 887 } 888 parseSortByElement(QDomElement & sortByElem,QgsFeatureRequest & featureRequest,const QString & typeName)889 void parseSortByElement( QDomElement &sortByElem, QgsFeatureRequest &featureRequest, const QString &typeName ) 890 { 891 QDomNodeList sortByNodes = sortByElem.childNodes(); 892 if ( sortByNodes.size() ) 893 { 894 for ( int i = 0; i < sortByNodes.size(); i++ ) 895 { 896 QDomElement sortPropElem = sortByNodes.at( i ).toElement(); 897 QDomNodeList sortPropChildNodes = sortPropElem.childNodes(); 898 if ( sortPropChildNodes.size() ) 899 { 900 QString fieldName; 901 bool ascending = true; 902 for ( int j = 0; j < sortPropChildNodes.size(); j++ ) 903 { 904 QDomElement sortPropChildElem = sortPropChildNodes.at( j ).toElement(); 905 if ( sortPropChildElem.tagName() == QLatin1String( "PropertyName" ) ) 906 { 907 fieldName = sortPropChildElem.text().trimmed(); 908 } 909 else if ( sortPropChildElem.tagName() == QLatin1String( "SortOrder" ) ) 910 { 911 QString sortOrder = sortPropChildElem.text().trimmed().toUpper(); 912 if ( sortOrder == QLatin1String( "DESC" ) || sortOrder == QLatin1String( "D" ) ) 913 ascending = false; 914 } 915 } 916 // clean fieldName 917 if ( fieldName.contains( ':' ) ) 918 { 919 fieldName = fieldName.section( ':', 1, 1 ); 920 } 921 if ( fieldName.contains( '/' ) ) 922 { 923 if ( fieldName.section( '/', 0, 0 ) != typeName ) 924 { 925 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 926 } 927 fieldName = fieldName.section( '/', 1, 1 ); 928 } 929 // addOrderBy 930 if ( !fieldName.isEmpty() ) 931 featureRequest.addOrderBy( fieldName, ascending ); 932 } 933 } 934 } 935 } 936 parseQueryElement(QDomElement & queryElem,const QgsProject * project)937 getFeatureQuery parseQueryElement( QDomElement &queryElem, const QgsProject *project ) 938 { 939 QString typeName = queryElem.attribute( QStringLiteral( "typeName" ), QString() ); 940 if ( typeName.contains( ':' ) ) 941 { 942 typeName = typeName.section( ':', 1, 1 ); 943 } 944 945 QgsFeatureRequest featureRequest; 946 QStringList serverFids; 947 QStringList propertyList; 948 QDomNodeList queryChildNodes = queryElem.childNodes(); 949 if ( queryChildNodes.size() ) 950 { 951 QDomElement sortByElem; 952 for ( int q = 0; q < queryChildNodes.size(); q++ ) 953 { 954 QDomElement queryChildElem = queryChildNodes.at( q ).toElement(); 955 if ( queryChildElem.tagName() == QLatin1String( "PropertyName" ) ) 956 { 957 QString fieldName = queryChildElem.text().trimmed(); 958 if ( fieldName.contains( ':' ) ) 959 { 960 fieldName = fieldName.section( ':', 1, 1 ); 961 } 962 if ( fieldName.contains( '/' ) ) 963 { 964 if ( fieldName.section( '/', 0, 0 ) != typeName ) 965 { 966 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 967 } 968 fieldName = fieldName.section( '/', 1, 1 ); 969 } 970 propertyList.append( fieldName ); 971 } 972 else if ( queryChildElem.tagName() == QLatin1String( "Filter" ) ) 973 { 974 featureRequest = parseFilterElement( typeName, queryChildElem, serverFids, project ); 975 } 976 else if ( queryChildElem.tagName() == QLatin1String( "SortBy" ) ) 977 { 978 sortByElem = queryChildElem; 979 } 980 } 981 parseSortByElement( sortByElem, featureRequest, typeName ); 982 } 983 984 // srsName attribute 985 QString srsName = queryElem.attribute( QStringLiteral( "srsName" ), QString() ); 986 987 getFeatureQuery query; 988 query.typeName = typeName; 989 query.srsName = srsName; 990 query.featureRequest = featureRequest; 991 query.serverFids = serverFids; 992 query.propertyList = propertyList; 993 return query; 994 } 995 996 namespace 997 { 998 static QSet< QString > sParamFilter 999 { 1000 QStringLiteral( "REQUEST" ), 1001 QStringLiteral( "FORMAT" ), 1002 QStringLiteral( "OUTPUTFORMAT" ), 1003 QStringLiteral( "BBOX" ), 1004 QStringLiteral( "FEATUREID" ), 1005 QStringLiteral( "TYPENAME" ), 1006 QStringLiteral( "FILTER" ), 1007 QStringLiteral( "EXP_FILTER" ), 1008 QStringLiteral( "MAXFEATURES" ), 1009 QStringLiteral( "STARTINDEX" ), 1010 QStringLiteral( "PROPERTYNAME" ), 1011 QStringLiteral( "_DC" ) 1012 }; 1013 1014 hitGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int numberOfFeatures,const QStringList & typeNames)1015 void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format, 1016 int numberOfFeatures, const QStringList &typeNames ) 1017 { 1018 QDateTime now = QDateTime::currentDateTime(); 1019 QString fcString; 1020 1021 if ( format == QgsWfsParameters::Format::GeoJSON ) 1022 { 1023 response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" ); 1024 fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); 1025 fcString += QStringLiteral( " \"timeStamp\": \"%1\"\n" ).arg( now.toString( Qt::ISODate ) ); 1026 fcString += QStringLiteral( " \"numberOfFeatures\": %1\n" ).arg( QString::number( numberOfFeatures ) ); 1027 fcString += QLatin1Char( '}' ); 1028 } 1029 else 1030 { 1031 if ( format == QgsWfsParameters::Format::GML2 ) 1032 response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" ); 1033 else 1034 response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" ); 1035 1036 //Prepare url 1037 QString hrefString = serviceUrl( request, project ); 1038 1039 QUrl mapUrl( hrefString ); 1040 1041 QUrlQuery query( mapUrl ); 1042 query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) ); 1043 //Set version 1044 if ( mWfsParameters.version().isEmpty() ) 1045 query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() ); 1046 else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1047 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) ); 1048 else 1049 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) ); 1050 1051 for ( auto param : query.queryItems() ) 1052 { 1053 if ( sParamFilter.contains( param.first.toUpper() ) ) 1054 query.removeAllQueryItems( param.first ); 1055 } 1056 1057 query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) ); 1058 query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) ); 1059 if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1060 { 1061 if ( format == QgsWfsParameters::Format::GML2 ) 1062 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) ); 1063 else 1064 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) ); 1065 } 1066 else 1067 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) ); 1068 1069 mapUrl.setQuery( query ); 1070 1071 hrefString = mapUrl.toString(); 1072 1073 QString wfsSchema; 1074 if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1075 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" ); 1076 else 1077 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" ); 1078 1079 //wfs:FeatureCollection valid 1080 fcString = QStringLiteral( "<wfs:FeatureCollection" ); 1081 fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\""; 1082 fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\""; 1083 fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\""; 1084 fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" ); 1085 fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" ); 1086 fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\""; 1087 fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" ); 1088 fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&" ) ) + "\""; 1089 fcString += "\n timeStamp=\"" + now.toString( Qt::ISODate ) + "\""; 1090 fcString += "\n numberOfFeatures=\"" + QString::number( numberOfFeatures ) + "\""; 1091 fcString += QLatin1String( ">\n" ); 1092 fcString += QLatin1String( "</wfs:FeatureCollection>" ); 1093 } 1094 1095 response.write( fcString.toUtf8() ); 1096 response.flush(); 1097 } 1098 startGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int prec,QgsCoordinateReferenceSystem & crs,QgsRectangle * rect,const QStringList & typeNames)1099 void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format, 1100 int prec, QgsCoordinateReferenceSystem &crs, QgsRectangle *rect, const QStringList &typeNames ) 1101 { 1102 QString fcString; 1103 1104 std::unique_ptr< QgsRectangle > transformedRect; 1105 1106 if ( format == QgsWfsParameters::Format::GeoJSON ) 1107 { 1108 response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" ); 1109 1110 if ( crs.isValid() && !rect->isEmpty() ) 1111 { 1112 QgsGeometry exportGeom = QgsGeometry::fromRect( *rect ); 1113 QgsCoordinateTransform transform; 1114 transform.setSourceCrs( crs ); 1115 transform.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); 1116 try 1117 { 1118 if ( exportGeom.transform( transform ) == 0 ) 1119 { 1120 transformedRect.reset( new QgsRectangle( exportGeom.boundingBox() ) ); 1121 rect = transformedRect.get(); 1122 } 1123 } 1124 catch ( QgsException &cse ) 1125 { 1126 Q_UNUSED( cse ) 1127 } 1128 } 1129 // EPSG:4326 max extent is -180, -90, 180, 90 1130 rect = new QgsRectangle( rect->intersect( QgsRectangle( -180.0, -90.0, 180.0, 90.0 ) ) ); 1131 1132 fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); 1133 fcString += " \"bbox\": [ " + qgsDoubleToString( rect->xMinimum(), prec ) + ", " + qgsDoubleToString( rect->yMinimum(), prec ) + ", " + qgsDoubleToString( rect->xMaximum(), prec ) + ", " + qgsDoubleToString( rect->yMaximum(), prec ) + "],\n"; 1134 fcString += QLatin1String( " \"features\": [\n" ); 1135 response.write( fcString.toUtf8() ); 1136 } 1137 else 1138 { 1139 if ( format == QgsWfsParameters::Format::GML2 ) 1140 response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" ); 1141 else 1142 response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" ); 1143 1144 //Prepare url 1145 QString hrefString = serviceUrl( request, project ); 1146 1147 QUrl mapUrl( hrefString ); 1148 1149 QUrlQuery query( mapUrl ); 1150 query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) ); 1151 //Set version 1152 if ( mWfsParameters.version().isEmpty() ) 1153 query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() ); 1154 else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1155 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) ); 1156 else 1157 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) ); 1158 1159 for ( auto param : query.queryItems() ) 1160 { 1161 if ( sParamFilter.contains( param.first.toUpper() ) ) 1162 query.removeAllQueryItems( param.first ); 1163 } 1164 1165 query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) ); 1166 query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) ); 1167 if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1168 { 1169 if ( format == QgsWfsParameters::Format::GML2 ) 1170 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) ); 1171 else 1172 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) ); 1173 } 1174 else 1175 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) ); 1176 1177 mapUrl.setQuery( query ); 1178 1179 hrefString = mapUrl.toString(); 1180 1181 QString wfsSchema; 1182 if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1183 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" ); 1184 else 1185 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" ); 1186 1187 //wfs:FeatureCollection valid 1188 fcString = QStringLiteral( "<wfs:FeatureCollection" ); 1189 fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\""; 1190 fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\""; 1191 fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\""; 1192 fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" ); 1193 fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" ); 1194 fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\""; 1195 fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" ); 1196 fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&" ) ) + "\""; 1197 fcString += QLatin1String( ">\n" ); 1198 1199 response.write( fcString.toUtf8() ); 1200 response.flush(); 1201 1202 QDomDocument doc; 1203 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1204 if ( format == QgsWfsParameters::Format::GML3 ) 1205 { 1206 QDomElement envElem = QgsOgcUtils::rectangleToGMLEnvelope( rect, doc, prec ); 1207 if ( !envElem.isNull() ) 1208 { 1209 if ( crs.isValid() ) 1210 { 1211 envElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1212 } 1213 bbElem.appendChild( envElem ); 1214 doc.appendChild( bbElem ); 1215 } 1216 } 1217 else 1218 { 1219 QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( rect, doc, prec ); 1220 if ( !boxElem.isNull() ) 1221 { 1222 if ( crs.isValid() ) 1223 { 1224 boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1225 } 1226 bbElem.appendChild( boxElem ); 1227 doc.appendChild( bbElem ); 1228 } 1229 } 1230 response.write( doc.toByteArray() ); 1231 response.flush(); 1232 } 1233 } 1234 setGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format,const QgsFeature & feature,int featIdx,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1235 void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx, 1236 const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1237 { 1238 if ( !feature.isValid() ) 1239 return; 1240 1241 if ( format == QgsWfsParameters::Format::GeoJSON ) 1242 { 1243 QString fcString; 1244 if ( featIdx == 0 ) 1245 fcString += QLatin1String( " " ); 1246 else 1247 fcString += QLatin1String( " ," ); 1248 mJsonExporter.setSourceCrs( params.crs ); 1249 mJsonExporter.setIncludeGeometry( false ); 1250 mJsonExporter.setIncludeAttributes( !params.attributeIndexes.isEmpty() ); 1251 mJsonExporter.setAttributes( params.attributeIndexes ); 1252 fcString += createFeatureGeoJSON( feature, params, pkAttributes ); 1253 fcString += QLatin1String( "\n" ); 1254 1255 response.write( fcString.toUtf8() ); 1256 } 1257 else 1258 { 1259 QDomDocument gmlDoc; 1260 QDomElement featureElement; 1261 if ( format == QgsWfsParameters::Format::GML3 ) 1262 { 1263 featureElement = createFeatureGML3( feature, gmlDoc, params, project, pkAttributes ); 1264 gmlDoc.appendChild( featureElement ); 1265 } 1266 else 1267 { 1268 featureElement = createFeatureGML2( feature, gmlDoc, params, project, pkAttributes ); 1269 gmlDoc.appendChild( featureElement ); 1270 } 1271 response.write( gmlDoc.toByteArray() ); 1272 } 1273 1274 // Stream partial content 1275 response.flush(); 1276 } 1277 endGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format)1278 void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format ) 1279 { 1280 QString fcString; 1281 if ( format == QgsWfsParameters::Format::GeoJSON ) 1282 { 1283 fcString += QLatin1String( " ]\n" ); 1284 fcString += QLatin1Char( '}' ); 1285 } 1286 else 1287 { 1288 fcString = QStringLiteral( "</wfs:FeatureCollection>\n" ); 1289 } 1290 response.write( fcString.toUtf8() ); 1291 } 1292 1293 createFeatureGeoJSON(const QgsFeature & feature,const createFeatureParams & params,const QgsAttributeList & pkAttributes)1294 QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams ¶ms, const QgsAttributeList &pkAttributes ) 1295 { 1296 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1297 //QgsJsonExporter force transform geometry to EPSG:4326 1298 //and the RFC 7946 GeoJSON specification recommends limiting coordinate precision to 6 1299 //Q_UNUSED( prec ) 1300 1301 //copy feature so we can modify its geometry as required 1302 QgsFeature f( feature ); 1303 QgsGeometry geom = feature.geometry(); 1304 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1305 { 1306 mJsonExporter.setIncludeGeometry( true ); 1307 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1308 { 1309 QgsRectangle box = geom.boundingBox(); 1310 f.setGeometry( QgsGeometry::fromRect( box ) ); 1311 } 1312 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1313 { 1314 f.setGeometry( geom.centroid() ); 1315 } 1316 } 1317 1318 return mJsonExporter.exportFeature( f, QVariantMap(), id ); 1319 } 1320 1321 createFeatureGML2(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1322 QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1323 { 1324 //gml:FeatureMember 1325 QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); 1326 1327 //qgs:%TYPENAME% 1328 QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ ); 1329 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1330 typeNameElement.setAttribute( QStringLiteral( "fid" ), id ); 1331 featureElement.appendChild( typeNameElement ); 1332 1333 //add geometry column (as gml) 1334 QgsGeometry geom = feature.geometry(); 1335 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1336 { 1337 int prec = params.precision; 1338 QgsCoordinateReferenceSystem crs = params.crs; 1339 QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); 1340 try 1341 { 1342 QgsGeometry transformed = geom; 1343 if ( transformed.transform( mTransform ) == 0 ) 1344 { 1345 geom = transformed; 1346 crs = params.outputCrs; 1347 if ( crs.isGeographic() && !params.crs.isGeographic() ) 1348 prec = std::min( params.precision + 3, 6 ); 1349 } 1350 } 1351 catch ( QgsCsException &cse ) 1352 { 1353 Q_UNUSED( cse ) 1354 } 1355 1356 QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) ); 1357 QDomElement gmlElem; 1358 QgsGeometry cloneGeom( geom ); 1359 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1360 { 1361 cloneGeom = QgsGeometry::fromRect( geom.boundingBox() ); 1362 } 1363 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1364 { 1365 cloneGeom = geom.centroid(); 1366 } 1367 else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) ) 1368 { 1369 cloneGeom.convertToMultiType(); 1370 } 1371 const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet(); 1372 if ( abstractGeom ) 1373 { 1374 gmlElem = abstractGeom->asGml2( doc, prec, "http://www.opengis.net/gml" ); 1375 } 1376 1377 if ( !gmlElem.isNull() ) 1378 { 1379 QgsRectangle box = geom.boundingBox(); 1380 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1381 QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( &box, doc, prec ); 1382 1383 if ( crs.isValid() ) 1384 { 1385 boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1386 gmlElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1387 } 1388 1389 bbElem.appendChild( boxElem ); 1390 typeNameElement.appendChild( bbElem ); 1391 1392 geomElem.appendChild( gmlElem ); 1393 typeNameElement.appendChild( geomElem ); 1394 } 1395 } 1396 1397 //read all attribute values from the feature 1398 QgsAttributes featureAttributes = feature.attributes(); 1399 QgsFields fields = feature.fields(); 1400 for ( int i = 0; i < params.attributeIndexes.count(); ++i ) 1401 { 1402 int idx = params.attributeIndexes[i]; 1403 if ( idx >= fields.count() ) 1404 { 1405 continue; 1406 } 1407 const QgsField field = fields.at( idx ); 1408 const QgsEditorWidgetSetup setup = field.editorWidgetSetup(); 1409 QString attributeName = field.name(); 1410 1411 QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 1412 QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) ); 1413 if ( featureAttributes[idx].isNull() ) 1414 { 1415 fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) ); 1416 } 1417 fieldElem.appendChild( fieldText ); 1418 typeNameElement.appendChild( fieldElem ); 1419 } 1420 1421 return featureElement; 1422 } 1423 createFeatureGML3(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1424 QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1425 { 1426 //gml:FeatureMember 1427 QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); 1428 1429 //qgs:%TYPENAME% 1430 QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ ); 1431 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1432 typeNameElement.setAttribute( QStringLiteral( "gml:id" ), id ); 1433 featureElement.appendChild( typeNameElement ); 1434 1435 //add geometry column (as gml) 1436 QgsGeometry geom = feature.geometry(); 1437 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1438 { 1439 int prec = params.precision; 1440 QgsCoordinateReferenceSystem crs = params.crs; 1441 QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); 1442 try 1443 { 1444 QgsGeometry transformed = geom; 1445 if ( transformed.transform( mTransform ) == 0 ) 1446 { 1447 geom = transformed; 1448 crs = params.outputCrs; 1449 if ( crs.isGeographic() && !params.crs.isGeographic() ) 1450 prec = std::min( params.precision + 3, 6 ); 1451 } 1452 } 1453 catch ( QgsCsException &cse ) 1454 { 1455 Q_UNUSED( cse ) 1456 } 1457 1458 QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) ); 1459 QDomElement gmlElem; 1460 QgsGeometry cloneGeom( geom ); 1461 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1462 { 1463 cloneGeom = QgsGeometry::fromRect( geom.boundingBox() ); 1464 } 1465 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1466 { 1467 cloneGeom = geom.centroid(); 1468 } 1469 else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) ) 1470 { 1471 cloneGeom.convertToMultiType(); 1472 } 1473 const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet(); 1474 if ( abstractGeom ) 1475 { 1476 gmlElem = abstractGeom->asGml3( doc, prec, "http://www.opengis.net/gml" ); 1477 } 1478 1479 if ( !gmlElem.isNull() ) 1480 { 1481 QgsRectangle box = geom.boundingBox(); 1482 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1483 QDomElement boxElem = QgsOgcUtils::rectangleToGMLEnvelope( &box, doc, prec ); 1484 1485 if ( crs.isValid() ) 1486 { 1487 boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1488 gmlElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1489 } 1490 1491 bbElem.appendChild( boxElem ); 1492 typeNameElement.appendChild( bbElem ); 1493 1494 geomElem.appendChild( gmlElem ); 1495 typeNameElement.appendChild( geomElem ); 1496 } 1497 } 1498 1499 //read all attribute values from the feature 1500 QgsAttributes featureAttributes = feature.attributes(); 1501 QgsFields fields = feature.fields(); 1502 for ( int i = 0; i < params.attributeIndexes.count(); ++i ) 1503 { 1504 int idx = params.attributeIndexes[i]; 1505 if ( idx >= fields.count() ) 1506 { 1507 continue; 1508 } 1509 1510 const QgsField field = fields.at( idx ); 1511 const QgsEditorWidgetSetup setup = field.editorWidgetSetup(); 1512 1513 QString attributeName = field.name(); 1514 1515 QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 1516 QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) ); 1517 if ( featureAttributes[idx].isNull() ) 1518 { 1519 fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) ); 1520 } 1521 fieldElem.appendChild( fieldText ); 1522 typeNameElement.appendChild( fieldElem ); 1523 } 1524 1525 return featureElement; 1526 } 1527 encodeValueToText(const QVariant & value,const QgsEditorWidgetSetup & setup)1528 QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup ) 1529 { 1530 if ( value.isNull() ) 1531 return QString(); 1532 1533 if ( setup.type() == QStringLiteral( "DateTime" ) ) 1534 { 1535 QgsDateTimeFieldFormatter fieldFormatter; 1536 const QVariantMap config = setup.config(); 1537 const QString fieldFormat = config.value( QStringLiteral( "field_format" ), fieldFormatter.defaultFormat( value.type() ) ).toString(); 1538 QDateTime date = value.toDateTime(); 1539 1540 if ( date.isValid() ) 1541 { 1542 return date.toString( fieldFormat ); 1543 } 1544 } 1545 else if ( setup.type() == QStringLiteral( "Range" ) ) 1546 { 1547 const QVariantMap config = setup.config(); 1548 if ( config.contains( QStringLiteral( "Precision" ) ) ) 1549 { 1550 // if precision is defined, use it 1551 bool ok; 1552 int precision( config[ QStringLiteral( "Precision" ) ].toInt( &ok ) ); 1553 if ( ok ) 1554 return QString::number( value.toDouble(), 'f', precision ); 1555 } 1556 } 1557 1558 switch ( value.type() ) 1559 { 1560 case QVariant::Int: 1561 case QVariant::UInt: 1562 case QVariant::LongLong: 1563 case QVariant::ULongLong: 1564 case QVariant::Double: 1565 return value.toString(); 1566 1567 case QVariant::Bool: 1568 return value.toBool() ? QStringLiteral( "true" ) : QStringLiteral( "false" ); 1569 1570 case QVariant::StringList: 1571 case QVariant::List: 1572 case QVariant::Map: 1573 { 1574 QString v = QgsJsonUtils::encodeValue( value ); 1575 1576 //do we need CDATA 1577 if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 ) 1578 v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) ); 1579 1580 return v; 1581 } 1582 1583 default: 1584 case QVariant::String: 1585 { 1586 QString v = value.toString(); 1587 1588 //do we need CDATA 1589 if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 ) 1590 v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) ); 1591 1592 return v; 1593 } 1594 } 1595 } 1596 1597 1598 } // namespace 1599 1600 } // namespace QgsWfs 1601 1602 1603 1604