1 /***************************************************************************
2 qgswfsshareddata.cpp
3 ---------------------
4 begin : March 2016
5 copyright : (C) 2016 by Even Rouault
6 email : even.rouault at spatialys.com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16 #include "qgswfsshareddata.h"
17 #include "qgswfsutils.h"
18 #include "qgscachedirectorymanager.h"
19 #include "qgsogcutils.h"
20 #include "qgsexpression.h"
21 #include "qgsmessagelog.h"
22 #include "qgslogger.h"
23 #include <QUrlQuery>
24
QgsWFSSharedData(const QString & uri)25 QgsWFSSharedData::QgsWFSSharedData( const QString &uri )
26 : QgsBackgroundCachedSharedData( "wfs", tr( "WFS" ) )
27 , mURI( uri )
28 {
29 mHideProgressDialog = mURI.hideDownloadProgressDialog();
30 mServerPrefersCoordinatesForTransactions_1_1 = mURI.preferCoordinatesForWfst11();
31 }
32
~QgsWFSSharedData()33 QgsWFSSharedData::~QgsWFSSharedData()
34 {
35 QgsDebugMsgLevel( QStringLiteral( "~QgsWFSSharedData()" ), 4 );
36
37 cleanup();
38 }
39
newFeatureDownloaderImpl(QgsFeatureDownloader * downloader,bool requestMadeFromMainThread)40 std::unique_ptr<QgsFeatureDownloaderImpl> QgsWFSSharedData::newFeatureDownloaderImpl( QgsFeatureDownloader *downloader, bool requestMadeFromMainThread )
41 {
42 return std::unique_ptr<QgsFeatureDownloaderImpl>( new QgsWFSFeatureDownloaderImpl( this, downloader, requestMadeFromMainThread ) );
43 }
44
isRestrictedToRequestBBOX() const45 bool QgsWFSSharedData::isRestrictedToRequestBBOX() const
46 {
47 return mURI.isRestrictedToRequestBBOX();
48 }
49
invalidateCacheBaseUnderLock()50 void QgsWFSSharedData::invalidateCacheBaseUnderLock()
51 {
52 }
53
54
srsName() const55 QString QgsWFSSharedData::srsName() const
56 {
57 QString srsName;
58 if ( !mSourceCrs.authid().isEmpty() )
59 {
60 if ( mWFSVersion.startsWith( QLatin1String( "1.0" ) ) ||
61 !mSourceCrs.authid().startsWith( QLatin1String( "EPSG:" ) ) ||
62 // For servers like Geomedia that advertise EPSG:XXXX in capabilities even in WFS 1.1 or 2.0
63 mCaps.useEPSGColumnFormat )
64 {
65 srsName = mSourceCrs.authid();
66 }
67 else
68 {
69 QStringList list = mSourceCrs.authid().split( ':' );
70 srsName = QStringLiteral( "urn:ogc:def:crs:EPSG::%1" ).arg( list.last() );
71 }
72 }
73 return srsName;
74 }
75
computeFilter(QString & errorMsg)76 bool QgsWFSSharedData::computeFilter( QString &errorMsg )
77 {
78 errorMsg.clear();
79 mWFSFilter.clear();
80 mSortBy.clear();
81
82 QgsOgcUtils::GMLVersion gmlVersion;
83 QgsOgcUtils::FilterVersion filterVersion;
84 bool honourAxisOrientation = false;
85 if ( mWFSVersion.startsWith( QLatin1String( "1.0" ) ) )
86 {
87 gmlVersion = QgsOgcUtils::GML_2_1_2;
88 filterVersion = QgsOgcUtils::FILTER_OGC_1_0;
89 }
90 else if ( mWFSVersion.startsWith( QLatin1String( "1.1" ) ) )
91 {
92 honourAxisOrientation = !mURI.ignoreAxisOrientation();
93 gmlVersion = QgsOgcUtils::GML_3_1_0;
94 filterVersion = QgsOgcUtils::FILTER_OGC_1_1;
95 }
96 else
97 {
98 honourAxisOrientation = !mURI.ignoreAxisOrientation();
99 gmlVersion = QgsOgcUtils::GML_3_2_1;
100 filterVersion = QgsOgcUtils::FILTER_FES_2_0;
101 }
102
103 if ( !mURI.sql().isEmpty() )
104 {
105 const QgsSQLStatement sql( mURI.sql() );
106
107 const QgsSQLStatement::NodeSelect *select = dynamic_cast<const QgsSQLStatement::NodeSelect *>( sql.rootNode() );
108 if ( !select )
109 {
110 // Makes Coverity happy, but cannot happen in practice
111 QgsDebugMsg( QStringLiteral( "should not happen" ) );
112 return false;
113 }
114 const QList<QgsSQLStatement::NodeColumnSorted *> orderBy = select->orderBy();
115 const auto constOrderBy = orderBy;
116 for ( QgsSQLStatement::NodeColumnSorted *columnSorted : constOrderBy )
117 {
118 if ( !mSortBy.isEmpty() )
119 mSortBy += QLatin1Char( ',' );
120 mSortBy += columnSorted->column()->name();
121 if ( !columnSorted->ascending() )
122 {
123 if ( mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
124 mSortBy += QLatin1String( " DESC" );
125 else
126 mSortBy += QLatin1String( " D" );
127 }
128 }
129
130 QDomDocument filterDoc;
131 const QDomElement filterElem = QgsOgcUtils::SQLStatementToOgcFilter(
132 sql, filterDoc, gmlVersion, filterVersion, mLayerPropertiesList,
133 honourAxisOrientation, mURI.invertAxisOrientation(),
134 mCaps.mapUnprefixedTypenameToPrefixedTypename,
135 &errorMsg );
136 if ( !errorMsg.isEmpty() )
137 {
138 errorMsg = tr( "SQL statement to OGC Filter error: " ) + errorMsg;
139 return false;
140 }
141 if ( !filterElem.isNull() )
142 {
143 filterDoc.appendChild( filterElem );
144 mWFSFilter = filterDoc.toString();
145 }
146 }
147 else
148 {
149 const QString filter( mURI.filter() );
150 if ( !filter.isEmpty() )
151 {
152 //test if filterString is already an OGC filter xml
153 QDomDocument filterDoc;
154 if ( filterDoc.setContent( filter ) )
155 {
156 mWFSFilter = filter;
157 }
158 else
159 {
160 //if not, if must be a QGIS expression
161 const QgsExpression filterExpression( filter );
162
163 const QDomElement filterElem = QgsOgcUtils::expressionToOgcFilter(
164 filterExpression, filterDoc, gmlVersion, filterVersion, mGeometryAttribute,
165 srsName(), honourAxisOrientation, mURI.invertAxisOrientation(),
166 &errorMsg );
167
168 if ( !errorMsg.isEmpty() )
169 {
170 errorMsg = tr( "Expression to OGC Filter error: " ) + errorMsg;
171 return false;
172 }
173 if ( !filterElem.isNull() )
174 {
175 filterDoc.appendChild( filterElem );
176 mWFSFilter = filterDoc.toString();
177 }
178 }
179 }
180 }
181
182 return true;
183 }
184
pushError(const QString & errorMsg) const185 void QgsWFSSharedData::pushError( const QString &errorMsg ) const
186 {
187 QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
188 emit raiseError( errorMsg );
189 }
190
createParser() const191 QgsGmlStreamingParser *QgsWFSSharedData::createParser() const
192 {
193 QgsGmlStreamingParser::AxisOrientationLogic axisOrientationLogic( QgsGmlStreamingParser::Honour_EPSG_if_urn );
194 if ( mURI.ignoreAxisOrientation() )
195 {
196 axisOrientationLogic = QgsGmlStreamingParser::Ignore_EPSG;
197 }
198
199 if ( !mLayerPropertiesList.isEmpty() )
200 {
201 QList< QgsGmlStreamingParser::LayerProperties > layerPropertiesList;
202 const auto constMLayerPropertiesList = mLayerPropertiesList;
203 for ( const QgsOgcUtils::LayerProperties &layerProperties : constMLayerPropertiesList )
204 {
205 QgsGmlStreamingParser::LayerProperties layerPropertiesOut;
206 layerPropertiesOut.mName = layerProperties.mName;
207 layerPropertiesOut.mGeometryAttribute = layerProperties.mGeometryAttribute;
208 layerPropertiesList << layerPropertiesOut;
209 }
210
211 return new QgsGmlStreamingParser( layerPropertiesList,
212 mFields,
213 mMapFieldNameToSrcLayerNameFieldName,
214 axisOrientationLogic,
215 mURI.invertAxisOrientation() );
216 }
217 else
218 {
219 return new QgsGmlStreamingParser( mURI.typeName(),
220 mGeometryAttribute,
221 mFields,
222 axisOrientationLogic,
223 mURI.invertAxisOrientation() );
224 }
225 }
226
227
getExtentFromSingleFeatureRequest() const228 QgsRectangle QgsWFSSharedData::getExtentFromSingleFeatureRequest() const
229 {
230 QgsWFSSingleFeatureRequest request( this );
231 return request.getExtent();
232 }
233
getFeatureCountFromServer() const234 long long QgsWFSSharedData::getFeatureCountFromServer() const
235 {
236 QgsWFSFeatureHitsRequest request( mURI );
237 return request.getFeatureCount( mWFSVersion, mWFSFilter, mCaps );
238 }
239
detectPotentialServerAxisOrderIssueFromSingleFeatureExtent() const240 bool QgsWFSSharedData::detectPotentialServerAxisOrderIssueFromSingleFeatureExtent() const
241 {
242 Q_ASSERT( !mComputedExtent.isNull() );
243 if ( mWFSVersion.startsWith( QLatin1String( "1.1" ) ) &&
244 !mURI.ignoreAxisOrientation() &&
245 !mURI.invertAxisOrientation() &&
246 mSourceCrs.hasAxisInverted() &&
247 mCapabilityExtent.contains( mComputedExtent ) )
248 {
249 pushError( QObject::tr( "It is likely that there is an issue with coordinate axis order of geometries when interacting with the server. You may want to enable the Ignore axis orientation and/or Invert axis orientation settings of the WFS connection." ) );
250 return true;
251 }
252 return false;
253 }
254
255 // -------------------------
256
257
QgsWFSFeatureHitsRequest(const QgsWFSDataSourceURI & uri)258 QgsWFSFeatureHitsRequest::QgsWFSFeatureHitsRequest( const QgsWFSDataSourceURI &uri )
259 : QgsWfsRequest( uri )
260 {
261 }
262
getFeatureCount(const QString & WFSVersion,const QString & filter,const QgsWfsCapabilities::Capabilities & caps)263 long long QgsWFSFeatureHitsRequest::getFeatureCount( const QString &WFSVersion,
264 const QString &filter, const QgsWfsCapabilities::Capabilities &caps )
265 {
266 const QString typeName = mUri.typeName();
267
268 QUrl getFeatureUrl( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
269 QUrlQuery query( getFeatureUrl );
270 query.addQueryItem( QStringLiteral( "VERSION" ), WFSVersion );
271 if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
272 {
273 query.addQueryItem( QStringLiteral( "TYPENAMES" ), typeName );
274 }
275 else
276 {
277 query.addQueryItem( QStringLiteral( "TYPENAME" ), typeName );
278 }
279
280 const QString namespaceValue( caps.getNamespaceParameterValue( WFSVersion, typeName ) );
281 if ( !namespaceValue.isEmpty() )
282 {
283 if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
284 {
285 query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
286 }
287 else
288 {
289 query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
290 }
291 }
292
293 if ( !filter.isEmpty() )
294 {
295 query.addQueryItem( QStringLiteral( "FILTER" ), filter );
296 }
297 query.addQueryItem( QStringLiteral( "RESULTTYPE" ), QStringLiteral( "hits" ) );
298
299 getFeatureUrl.setQuery( query );
300 if ( !sendGET( getFeatureUrl, QString(), true ) )
301 return -1;
302
303 const QByteArray &buffer = response();
304
305 QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSFeatureHitsRequest: " ) + buffer, 4 );
306
307 // parse XML
308 QString error;
309 QDomDocument domDoc;
310 if ( !domDoc.setContent( buffer, true, &error ) )
311 {
312 QgsDebugMsg( QStringLiteral( "parsing failed: " ) + error );
313 return -1;
314 }
315
316 const QDomElement doc = domDoc.documentElement();
317 const QString numberOfFeatures =
318 ( WFSVersion.startsWith( QLatin1String( "1.1" ) ) ) ? doc.attribute( QStringLiteral( "numberOfFeatures" ) ) :
319 /* 2.0 */ doc.attribute( QStringLiteral( "numberMatched" ) );
320 if ( !numberOfFeatures.isEmpty() )
321 {
322 bool isValid;
323 const long long ret = numberOfFeatures.toLongLong( &isValid );
324 if ( !isValid )
325 return -1;
326 return ret;
327 }
328
329 return -1;
330 }
331
errorMessageWithReason(const QString & reason)332 QString QgsWFSFeatureHitsRequest::errorMessageWithReason( const QString &reason )
333 {
334 return tr( "Download of feature count failed: %1" ).arg( reason );
335 }
336
337
338 // -------------------------
339
340
QgsWFSSingleFeatureRequest(const QgsWFSSharedData * shared)341 QgsWFSSingleFeatureRequest::QgsWFSSingleFeatureRequest( const QgsWFSSharedData *shared )
342 : QgsWfsRequest( shared->mURI ), mShared( shared )
343 {
344 }
345
getExtent()346 QgsRectangle QgsWFSSingleFeatureRequest::getExtent()
347 {
348 QUrl getFeatureUrl( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
349 QUrlQuery query( getFeatureUrl );
350 query.addQueryItem( QStringLiteral( "VERSION" ), mShared->mWFSVersion );
351 if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
352 query.addQueryItem( QStringLiteral( "TYPENAMES" ), mUri.typeName() );
353 else
354 query.addQueryItem( QStringLiteral( "TYPENAME" ), mUri.typeName() );
355
356 const QString namespaceValue( mShared->mCaps.getNamespaceParameterValue( mShared->mWFSVersion, mUri.typeName() ) );
357 if ( !namespaceValue.isEmpty() )
358 {
359 if ( mShared->mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
360 query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
361 else
362 query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
363 }
364
365 if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
366 query.addQueryItem( QStringLiteral( "COUNT" ), QString::number( 1 ) );
367 else
368 query.addQueryItem( QStringLiteral( "MAXFEATURES" ), QString::number( 1 ) );
369
370 getFeatureUrl.setQuery( query );
371 if ( !sendGET( getFeatureUrl, QString(), true ) )
372 return QgsRectangle();
373
374 const QByteArray &buffer = response();
375
376 QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSSingleFeatureRequest: " ) + buffer, 4 );
377
378 // parse XML
379 QgsGmlStreamingParser *parser = mShared->createParser();
380 QString gmlProcessErrorMsg;
381 QgsRectangle extent;
382 if ( parser->processData( buffer, true, gmlProcessErrorMsg ) )
383 {
384 QVector<QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair> featurePtrList =
385 parser->getAndStealReadyFeatures();
386 for ( int i = 0; i < featurePtrList.size(); i++ )
387 {
388 const QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair &featPair = featurePtrList[i];
389 const QgsFeature f( *( featPair.first ) );
390 const QgsGeometry geometry = f.geometry();
391 if ( !geometry.isNull() )
392 {
393 extent = geometry.boundingBox();
394 }
395 delete featPair.first;
396 }
397 }
398 delete parser;
399 return extent;
400 }
401
errorMessageWithReason(const QString & reason)402 QString QgsWFSSingleFeatureRequest::errorMessageWithReason( const QString &reason )
403 {
404 return tr( "Download of feature failed: %1" ).arg( reason );
405 }
406
407