incremental-wsf-rev3.patch

WFS incremental fetch patch rev. 3 (replaces prior versions) - Bill Clay, 2011-12-31 07:46 AM

Download (43 KB)

View differences:

src/gui/qgsexpressionbuilderwidget.cpp
281 281
  }
282 282
  else
283 283
  {
284
    lblPreview->setText( tr( "Expression is valid" ) );
284 285
    lblPreview->setStyleSheet( "" );
285 286
    txtExpressionString->setToolTip( "" );
286 287
    lblPreview->setToolTip( "" );
src/providers/wfs/qgswfsconnection.cpp
9 9
#include <QStringList>
10 10

  
11 11
static const QString WFS_NAMESPACE = "http://www.opengis.net/wfs";
12
static const QString OGC_NAMESPACE = "http://www.opengis.net/ogc";
12 13

  
13 14
QgsWFSConnection::QgsWFSConnection( QString connName, QObject *parent ) :
14 15
    QObject( parent ),
......
220 221
    mCaps.featureTypes.append( featureType );
221 222
  }
222 223

  
224
  // get filter capability
225
  QDomNodeList fcap = capabilitiesDocument.elementsByTagNameNS( OGC_NAMESPACE, "Filter_Capabilities" );
226
  mCaps.canFilter = ( fcap.size() && fcap.at( 0 ).hasChildNodes() );
227

  
223 228
  mCapabilitiesReply->deleteLater();
224 229
  mCapabilitiesReply = 0;
225 230
  emit gotCapabilities();
src/providers/wfs/qgswfsconnection.h
48 48
    //! parsed get capabilities document
49 49
    struct GetCapabilities
50 50
    {
51
      void clear() { featureTypes.clear(); }
51
      void clear() { featureTypes.clear(); canFilter = false; }
52 52

  
53
      bool canFilter;  //provider advertises <Filter> capability
53 54
      QList<FeatureType> featureTypes;
54 55
    };
55 56

  
src/providers/wfs/qgswfsdata.cpp
47 47
    mThematicAttributes( thematicAttributes ),
48 48
    mWkbType( wkbType ),
49 49
    mFinished( false ),
50
    mFeatureCount( 0 )
50
    mFeatureCount( /*0 [wbc 111225 incremental get*/ mFeatures.count() )
51 51
{
52 52
  //find out mTypeName from uri
53 53
  QStringList arguments = uri.split( "&" );
......
75 75

  
76 76
}
77 77

  
78
int QgsWFSData::getWFSData()
78
int QgsWFSData::getWFSData( const QSet<QString>* wfsFids )
79 79
{
80
  QgsDebugMsg( QString( "begin fetch: %1 cached features; %2 discard candidates" )
81
                        .arg( mFeatureCount ).arg( wfsFids ? wfsFids->size() : 0 ) );
82

  
83
  mWfsFids = wfsFids;
84

  
80 85
  XML_Parser p = XML_ParserCreateNS( NULL, NS_SEPARATOR );
81 86
  XML_SetUserData( p, this );
82 87
  XML_SetElementHandler( p, QgsWFSData::start, QgsWFSData::end );
......
142 147

  
143 148
void QgsWFSData::setFinished( )
144 149
{
150
  QgsDebugMsg( QString( "end fetch: %1 features in cache" )
151
                        .arg( mFeatureCount ) );
145 152
  mFinished = true;
146 153
}
147 154

  
......
301 308
    }
302 309
  }
303 310
  else if ( elementName == GML_NAMESPACE + NS_SEPARATOR + "featureMember" )
304
  {
305
    //MH090531: Check if all feature attributes are initialised, sometimes attribute values are missing.
306
    //We fill the not initialized ones with empty strings, otherwise the feature cannot be exported to shp later
307
    QgsAttributeMap currentFeatureAttributes = mCurrentFeature->attributeMap();
308
    QMap<QString, QPair<int, QgsField> >::const_iterator att_it = mThematicAttributes.constBegin();
309
    for ( ; att_it != mThematicAttributes.constEnd(); ++att_it )
311
  { //ignore duplicate features (e.g., protrude beyond previous extent)
312
    //[wbc111228 issue 4280: fetch un-cached features on demand]
313
    if ( mWfsFids && ! mCurrentFeatureId.isEmpty() && mWfsFids->contains( mCurrentFeatureId ) )
314
    {
315
      QgsDebugMsg( QString( "WFS FID %1 already fetched QGIS id %2" )
316
                            .arg( mCurrentFeatureId )
317
                            .arg( mIdMap.key ( mCurrentFeatureId ) ) );
318
    }
319
    else
310 320
    {
311
      int attIndex = att_it.value().first;
312
      QgsAttributeMap::const_iterator findIt = currentFeatureAttributes.find( attIndex );
313
      if ( findIt == currentFeatureAttributes.constEnd() )
321
      //MH090531: Check if all feature attributes are initialised, sometimes attribute values are missing.
322
      //We fill the not initialized ones with empty strings, otherwise the feature cannot be exported to shp later
323
      QgsAttributeMap currentFeatureAttributes = mCurrentFeature->attributeMap();
324
      QMap<QString, QPair<int, QgsField> >::const_iterator att_it = mThematicAttributes.constBegin();
325
      for ( ; att_it != mThematicAttributes.constEnd(); ++att_it )
314 326
      {
315
        mCurrentFeature->addAttribute( attIndex, QVariant( "" ) );
327
        int attIndex = att_it.value().first;
328
        QgsAttributeMap::const_iterator findIt = currentFeatureAttributes.find( attIndex );
329
        if ( findIt == currentFeatureAttributes.constEnd() )
330
        {
331
          mCurrentFeature->addAttribute( attIndex, QVariant( "" ) );
332
        }
316 333
      }
317
    }
318

  
319 334

  
320
    mCurrentFeature->setGeometryAndOwnership( mCurrentWKB, mCurrentWKBSize );
321
    mFeatures.insert( mCurrentFeature->id(), mCurrentFeature );
322
    if ( !mCurrentFeatureId.isEmpty() )
323
    {
324
      mIdMap.insert( mCurrentFeature->id(), mCurrentFeatureId );
335
      mCurrentFeature->setGeometryAndOwnership( mCurrentWKB, mCurrentWKBSize );
336
      mFeatures.insert( mCurrentFeature->id(), mCurrentFeature );
337
      if ( !mCurrentFeatureId.isEmpty() )
338
      {
339
        mIdMap.insert( mCurrentFeature->id(), mCurrentFeatureId );
340
      }
341
      ++mFeatureCount;
325 342
    }
326
    ++mFeatureCount;
327 343
    mParseModeStack.pop();
328 344
  }
329 345
  else if ( elementName == GML_NAMESPACE + NS_SEPARATOR + "Point" )
src/providers/wfs/qgswfsdata.h
51 51
       @param srs the reference system of the layer
52 52
       @param features the features of the layer
53 53
    @return 0 in case of success*/
54
    int getWFSData();
54
    int getWFSData( const QSet<QString>* wfsFids = (QSet<QString>*) 0 );
55 55

  
56 56
  private slots:
57 57
    void setFinished();
......
175 175
    QString mCoordinateSeparator;
176 176
    /**Tuple separator for coordinate strings. Usually " " */
177 177
    QString mTupleSeparator;
178
    /**incremental get: optional list of FIDs to be ignored in WFS input stream*/
179
    const QSet<QString>* mWfsFids;
178 180
};
179 181

  
180 182
#endif
src/providers/wfs/qgswfsprovider.cpp
17 17

  
18 18
#define WFS_THRESHOLD 200
19 19

  
20
#include "qgis.h"
21 20
#include "qgsapplication.h"
22 21
#include "qgsmaplayerregistry.h"
23 22
#include "qgsfeature.h"
......
49 48
QgsWFSProvider::QgsWFSProvider( const QString& uri )
50 49
    : QgsVectorDataProvider( uri ),
51 50
    mNetworkRequestFinished( true ),
51
    mGeometryAttribute( 0 ),
52 52
    mEncoding( QgsWFSProvider::GET ),
53 53
    mUseIntersect( false ),
54 54
    mWKBType( QGis::WKBUnknown ),
......
57 57
    mValid( true ),
58 58
    mLayer( 0 ),
59 59
    mGetRenderedOnly( false ),
60
    mInitGro( false )
60
    mInitUri(),
61
    mGetExtent(),
62
    mWfsFids( 0 ),
63
    mFilterCaps( 0 )
61 64
{
62 65
  mSpatialIndex = 0;
63 66
  if ( uri.isEmpty() )
......
66 69
    return;
67 70
  }
68 71

  
69
  //Local url or HTTP?  [WBC 111221] refactored from getFeature()
72
  //Local url or HTTP?  refactored from getFeature()
73
  //[wbc111228 issue 4280: fetch un-cached features on demand]
70 74
  if ( uri.startsWith( "http" ) )
71 75
  {
72 76
    mEncoding = QgsWFSProvider::GET;
......
75 79
  {
76 80
    mEncoding = QgsWFSProvider::FILE;
77 81
  }
78

  
79
  //create mSourceCRS from url if possible [WBC 111221] refactored from GetFeatureGET()
82
  //create mSourceCRS from url if possible; refactored from GetFeatureGET()
80 83
  QString srsname = parameterFromUrl( "SRSNAME" );
81 84
  if ( !srsname.isEmpty() )
82 85
  {
83 86
    mSourceCRS.createFromOgcWmsCrs( srsname );
84 87
  }
85

  
86 88
  //fetch attributes of layer and type of its geometry attribute
87
  //WBC 111221: extracting geometry type here instead of getFeature allows successful
89
  //extracting geometry type before getFeature usually allows successful
88 90
  //layer creation even when no features are retrieved (due to, e.g., BBOX or FILTER)
89
  if ( describeFeatureType( uri, mGeometryAttribute, mFields, mWKBType ) )
91
  QString describeFeatureUri = uri;
92
  describeFeatureUri.replace( QString( "GetFeature" ), QString( "DescribeFeatureType" ) );
93
  if ( describeFeatureType( describeFeatureUri, mGeometryAttribute, mFields, mWKBType ) )
90 94
  {
91
    mValid = false;
92 95
    QgsDebugMsg( QString( "describeFeatureType failed, URI=%1" ).arg( uri ) );
93
    QMessageBox( QMessageBox::Warning, "DescribeFeatureType failed!",
94
                 QString( "Layer cannot be created from\n%1" ).arg( uri ) );
96
    QMessageBox::critical ( 0, tr( "DescribeFeatureType failed!" ),
97
                 tr( "Layer cannot be created from\n%1" ).arg( uri ) );
98
    mValid = false;
95 99
    return;
96 100
  }
97

  
98
  if ( ! uri.contains( "BBOX" ) )
99
  { //"Cache Features" option; get all features in layer immediately
101
  //fetch features now or on first render?
102
  if ( mEncoding == QgsWFSProvider::FILE || ! uri.contains("BBOX") || mWKBType == QGis::WKBUnknown )
103
  { //now if (1) local URL, (2) "Cache All Features" option, or (3) WFS describeFeatureType does
104
    //not disclose a specific geometry type (e.g., TinyOWS reports "GeometryPropertyType")
105
    //retrieve all features in layer immediately
100 106
    reloadData();
101
  } //otherwise, defer feature retrieval until layer is first rendered
107
  } //otherwise: defer feature fetch until layer is first rendered
108
  else
109
  {
110
    mSpatialIndex = new QgsSpatialIndex();
111
  }
112
  //if we still do not know geometry type, user needs to change source select parameters
113
  if ( mWKBType == QGis::WKBUnknown )
114
  {
115
    QgsDebugMsg( QString( "Cannot obtain geometry type, URI=%1" ).arg( uri ) );
116
    QMessageBox::critical ( 0, tr( "Cannot obtain geometry type!" ),
117
                 tr( "This layer contains no geometry features in the current map canvas extent. "
118
                 "Reposition the map or check \"Cache All Features\" in the layer selection dialog." ) );
119
    mValid = false;
120
  }
121
  //[end mods for issue 4280]
102 122

  
103 123
  if ( mValid )
104 124
  {
......
118 138
  delete mSpatialIndex;
119 139
  mSpatialIndex = new QgsSpatialIndex();
120 140
  mValid = !getFeature( dataSourceUri() );
141
  if ( mValid )
142
  { //record fetched extent per stand-alone BBOX (if any) in getFeature URI
143
    mGetExtent = getUriBBox();
144
  }
145
  else
146
  { //getFeature failed; clear getFeature fetch extent
147
    mGetExtent = QgsRectangle();
148
  }
121 149
}
122 150

  
123 151
void QgsWFSProvider::deleteData()
......
128 156
    delete mFeatures[i];
129 157
  }
130 158
  mFeatures.clear();
159
  mFeatureCount = 0;
160
  mIdMap.clear();
131 161
}
132 162

  
133 163
void QgsWFSProvider::copyFeature( QgsFeature* f, QgsFeature& feature, bool fetchGeometry, QgsAttributeList fetchAttributes )
......
269 299
  mAttributesToFetch = fetchAttributes;
270 300
  mFetchGeom = fetchGeometry;
271 301

  
302
  //first time through, initialize GetRenderedOnly logic
303
  //(layer object not available during ctor initialization)
304
  //[wbc111228 issue 4604: fetch rendered features only]
305
  //[issue 4280: fetch un-cached features on demand]
306
  if ( mEncoding == QgsWFSProvider::GET && mInitUri.isEmpty() )
307
  { //did user check "Cache All Features" in WFS layer source selection?
308
    if ( dataSourceUri().contains( "BBOX" ) )
309
    { //no: initialize incremental getFeature
310
      mGetRenderedOnly = initGetRenderedOnly( rect );
311
    }
312
    //getRenderedOnly manipulates dataSourceUri; save original URI
313
    mInitUri = dataSourceUri();
314
  }
315

  
272 316
  if ( rect.isEmpty() )
273
  { //select all features
317
  { //select all previously-retrieved features
274 318
    mSpatialFilter = mExtent;
275 319
    mSelectedFeatures = mFeatures.keys();
276 320
  }
277 321
  else
278
  { //select features intersecting caller's extent
279
    QString dsURI = dataSourceUri();
280
    //first time through, initialize GetRenderedOnly args
281
    //ctor cannot initialize because layer object not available then
282
    if ( ! mInitGro )
283
    { //did user check "Cache Features" in WFS layer source selection?
284
      if ( dsURI.contains( "BBOX" ) )
285
      { //no: initialize incremental getFeature
286
        if ( initGetRenderedOnly( rect ) )
287
        {
288
          mGetRenderedOnly = true;
289
        }
290
        else
291
        { //initialization failed;
292
          QgsDebugMsg( QString( "GetRenderedOnly initialization failed; incorrect operation may occur\n%1" )
293
                       .arg( dataSourceUri() ) );
294
          QMessageBox( QMessageBox::Warning, "Non-Cached layer initialization failed!",
295
                       QString( "Incorrect operation may occur:\n%1" ).arg( dataSourceUri() ) );
296
        }
297
      }
298
      mInitGro = true;
299
    }
300

  
322
  { //select features intersecting caller's extent (e.g., map canvas extent)
301 323
    if ( mGetRenderedOnly )
302
    { //"Cache Features" was not selected for this layer
303
      //has rendered extent expanded beyond last-retrieved WFS extent?
304
      //NB: "intersect" instead of "contains" tolerates rounding errors;
305
      //    avoids unnecessary second fetch on zoom-in/zoom-out sequences
306
      QgsRectangle olap( rect );
307
      olap = olap.intersect( &mGetExtent );
308
      if ( doubleNear( rect.width(), olap.width() ) && doubleNear( rect.height(), olap.height() ) )
309
      { //difference between canvas and layer extents is within rounding error: do not re-fetch
310
        QgsDebugMsg( QString( "Layer %1 GetRenderedOnly: no fetch required" ).arg( mLayer->name() ) );
311
      }
312
      else
313
      { //combined old and new extents might speed up local panning & zooming
314
        mGetExtent.combineExtentWith( &rect );
315
        //but see if the combination is useless or too big
316
        double pArea = mGetExtent.width() * mGetExtent.height();
317
        double cArea = rect.width() * rect.height();
318
        if ( olap.isEmpty() || pArea > ( cArea * 4.0 ) )
319
        { //new canvas extent does not overlap or combining old and new extents would
320
          //fetch > 4 times the area to be rendered; get only what will be rendered
321
          mGetExtent = rect;
322
        }
323
        QgsDebugMsg( QString( "Layer %1 GetRenderedOnly: fetching extent %2" )
324
                     .arg( mLayer->name(), mGetExtent.asWktCoordinates() ) );
325
        dsURI = dsURI.replace( QRegExp( "BBOX=[^&]*" ),
326
                               QString( "BBOX=%1,%2,%3,%4" )
327
                               .arg( mGetExtent.xMinimum(), 0, 'f' )
328
                               .arg( mGetExtent.yMinimum(), 0, 'f' )
329
                               .arg( mGetExtent.xMaximum(), 0, 'f' )
330
                               .arg( mGetExtent.yMaximum(), 0, 'f' ) );
331
        //TODO: BBOX may not be combined with FILTER. WFS spec v. 1.1.0, sec. 14.7.3 ff.
332
        //      if a FILTER is present, the BBOX must be merged into it, capabilities permitting.
333
        //      Else one criterion must be abandoned and the user warned.  [WBC 111221]
334
        setDataSourceUri( dsURI );
335
        reloadData();
336
        mLayer->updateExtents();
337
      }
324
    { //"Cache All Features" was not selected for this layer; fetch features to be rendered
325
      //[issues 4604 + 4280]
326
      incrementalGet( rect );
338 327
    }
339 328

  
340 329
    mSpatialFilter = rect;
......
713 702
}
714 703

  
715 704
int QgsWFSProvider::describeFeatureType( const QString& uri, QString& geometryAttribute,
716
    QgsFieldMap& fields, QGis::WkbType& geomType )
705
                                         QgsFieldMap& fields, QGis::WkbType& geomType )
717 706
//NB: also called from QgsWFSSourceSelect::on_treeWidget_itemDoubleClicked() to build filters.
718 707
//    a temporary provider object is constructed with a null URI, which bypasses much provider
719 708
//    instantiation logic: refresh(), getFeature(), etc.  therefore, many provider class members
720 709
//    are only default values or uninitialized when running under the source select dialog!
721 710
{
722 711
  fields.clear();
723
  //Local url or HTTP?  WBC111221 refactored here from getFeature()
712
  //Local url or HTTP?
724 713
  switch ( mEncoding )
725 714
  {
726 715
    case QgsWFSProvider::GET:
727
    {
728 716
      return describeFeatureTypeGET( uri, geometryAttribute, fields, geomType );
729
    }
730 717
    case QgsWFSProvider::FILE:
731
    {
732 718
      return describeFeatureTypeFile( uri, geometryAttribute, fields, geomType );
733
    }
734 719
  }
735 720
  QgsDebugMsg( "SHOULD NOT OCCUR: mEncoding undefined" );
736 721
  return 1;
......
768 753
    QObject::connect( this, SIGNAL( dataReadProgressMessage( QString ) ), mainWindow, SLOT( showStatusMessage( QString ) ) );
769 754
  }
770 755

  
771
  if ( dataReader.getWFSData() != 0 )
756
  if ( dataReader.getWFSData( mWfsFids ) != 0 )
772 757
  {
773 758
    QgsDebugMsg( "getWFSData returned with error" );
774 759
    return 1;
......
777 762
  QgsDebugMsg( QString( "feature count after request is: %1" ).arg( mFeatures.size() ) );
778 763
  QgsDebugMsg( QString( "mExtent after request is: %1" ).arg( mExtent.toString() ) );
779 764

  
780
  for ( QMap<QgsFeatureId, QgsFeature*>::iterator it = mFeatures.begin(); it != mFeatures.end(); ++it )
765
  for ( QMap<QgsFeatureId, QgsFeature*>::iterator it = mFeatures.find( mFeatureCount ); it != mFeatures.end(); ++it )
781 766
  {
782
    QgsDebugMsg( "feature " + FID_TO_STRING(( *it )->id() ) );
767
    //QgsDebugMsg( "feature " + FID_TO_STRING(( *it )->id() ) );
783 768
    mSpatialIndex->insertFeature( *( it.value() ) );
784 769
  }
785 770

  
......
832 817

  
833 818
  mNetworkRequestFinished = false;
834 819

  
835
  QString describeFeatureUri = uri;
836
  describeFeatureUri.replace( QString( "GetFeature" ), QString( "DescribeFeatureType" ) );
837
  QNetworkRequest request( describeFeatureUri );
820
  QNetworkRequest request( uri );
838 821
  QNetworkReply* reply = QgsNetworkAccessManager::instance()->get( request );
839 822
  connect( reply, SIGNAL( finished() ), this, SLOT( networkRequestFinished() ) );
840 823
  while ( !mNetworkRequestFinished )
......
867 850
  mNetworkRequestFinished = true;
868 851
}
869 852

  
870
int QgsWFSProvider::describeFeatureTypeFile( const QString& uri, QString& geometryAttribute, QgsFieldMap& fields, QGis::WkbType& geomType )
853
int QgsWFSProvider::describeFeatureTypeFile( const QString& uri, QString& geometryAttribute, QgsFieldMap& fields, QGis::WkbType& geomType)
871 854
{
872 855
  //first look in the schema file
873 856
  QString noExtension = uri;
......
910 893

  
911 894
int QgsWFSProvider::readAttributesFromSchema( QDomDocument& schemaDoc, QString& geometryAttribute, QgsFieldMap& fields, QGis::WkbType& geomType )
912 895
{
896
  QgsDebugMsg( schemaDoc.toString() );
913 897
  //get the <schema> root element
914 898
  QDomNodeList schemaNodeList = schemaDoc.elementsByTagNameNS( "http://www.w3.org/2001/XMLSchema", "schema" );
915 899
  if ( schemaNodeList.length() < 1 )
......
2323 2307
  }
2324 2308

  
2325 2309
  mCapabilities = capabilities;
2310

  
2311
  //look for filter capabilities used by incremental get
2312
  QDomElement e = capabilitiesDocument.elementsByTagNameNS( "http://www.opengis.net/ogc", "Filter_Capabilities" ).item(0).toElement();
2313
  if ( ! e.tagName().isEmpty() )
2314
    {
2315
    mFilterCaps |= Filter;
2316
    QDomNodeList o = e.elementsByTagNameNS( "http://www.opengis.net/ogc", "Logical_Operators" );
2317
    if ( o.size() )
2318
    {
2319
      mFilterCaps |= LogicOps;
2320
    }
2321
    o = e.elementsByTagNameNS( "http://www.opengis.net/ogc", "BBOX" );
2322
    if ( o.size() )
2323
    {
2324
      mFilterCaps |= BBox;
2325
    }
2326
  }
2326 2327
}
2327 2328

  
2328 2329
void QgsWFSProvider::appendSupportedOperations( const QDomElement& operationsElem, int& capabilities ) const
......
2353 2354
}
2354 2355

  
2355 2356
//initialization for getRenderedOnly option
2356
//(formerly "Only request features overlapping the current view extent")
2357
//[wbc111228 issue 4604: fetch rendered features only]
2358
//[issue 4280: fetch un-cached features on demand]
2357 2359
bool QgsWFSProvider::initGetRenderedOnly( const QgsRectangle rect )
2358 2360
{ //find our layer
2361
  Q_UNUSED( rect );
2359 2362
  QMap<QString, QgsMapLayer*> layers = QgsMapLayerRegistry::instance()->mapLayers();
2360 2363
  QMap<QString, QgsMapLayer*>::const_iterator layersIt = layers.begin();
2361 2364
  for ( ; layersIt != layers.end() ; ++layersIt )
2362 2365
  {
2363
    if (( mLayer = dynamic_cast<QgsVectorLayer*>( layersIt.value() ) ) )
2366
    if ( ( mLayer = dynamic_cast<QgsVectorLayer*>( layersIt.value() ) ) )
2364 2367
    {
2365 2368
      if ( mLayer->dataProvider() == this )
2366 2369
      {
......
2371 2374
  }
2372 2375
  if ( layersIt == layers.end() )
2373 2376
  {
2374
    QgsDebugMsg( "SHOULD NOT OCCUR: initialize() did not find layer." );
2377
    QgsDebugMsg( QString( "SHOULD NOT OCCUR: initialize() did not find layer for %1." )
2378
                 .arg( dataSourceUri() ) );
2379
    QMessageBox::critical ( 0, tr( "Non-Cached layer initialization failed!" ),
2380
                 tr( "Incorrect operation may occur:\n%1" ).arg( dataSourceUri() ) );
2375 2381
    return false;
2376 2382
  }
2377 2383
  return true;
......
2379 2385

  
2380 2386
QGis::WkbType QgsWFSProvider::geomTypeFromPropertyType( QString attName, QString propType )
2381 2387
{
2382
  const QStringList geomTypes = ( QStringList()
2383
                                  //all GML v.2.1.3 _geometryProperty group members, except MultiGeometryPropertyType
2384
                                  //sequence must exactly match enum Qgis::WkbType
2385
                                  << ""  // unknown geometry, enum 0
2386
                                  << "Point"
2387
                                  << "LineString"
2388
                                  << "Polygon"
2389
                                  << "MultiPoint"
2390
                                  << "MultiLineString"
2391
                                  << "MultiPolygon" );
2388
  const QStringList geomTypes = ( QStringList ()
2389
    //all GML v.2.1.3 _geometryProperty group members, except MultiGeometryPropertyType
2390
    //sequence must exactly match enum Qgis::WkbType
2391
    << ""  // unknown geometry, enum 0
2392
    << "Point"
2393
    << "LineString"
2394
    << "Polygon"
2395
    << "MultiPoint"
2396
    << "MultiLineString"
2397
    << "MultiPolygon" );
2392 2398

  
2393 2399
  QgsDebugMsg( QString( "DescribeFeatureType geometry attribute \"%1\" type is \"%2\"" )
2394
               .arg( attName, propType ) );
2400
                       .arg( attName, propType ) );
2395 2401
  int i = geomTypes.indexOf( propType );
2396 2402
  if ( i <= 0 )
2397 2403
  { // feature type missing or unknown
2398
    i = ( int ) QGis::WKBUnknown;
2404
    i = (int) QGis::WKBUnknown;
2405
  }
2406
  return (QGis::WkbType) i;
2407
}
2408

  
2409
//if not caching whole layer, add features to the cache incrementally
2410
//as pans and zooms expose new areas on the map canvas
2411
//[wbc111228 issue 4280: fetch un-cached features on demand]
2412
void QgsWFSProvider::incrementalGet( QgsRectangle rect )
2413
{
2414
 QgsDebugMsg( QString( "\nprevious get extent %1\n      select extent %2" ).arg( mGetExtent.toString(), rect.toString() ) );
2415
 //has rendered extent expanded beyond last-retrieved WFS extent?
2416
  //NB: compare overlap area instead of using "contains" function
2417
  //    avoids double fetch due to rounding error on zoom-in/zoom-out sequences
2418
  QgsRectangle olap( rect );
2419
  olap = olap.intersect( &mGetExtent );
2420
  //is there any overlap between current layer extent and new canvas?
2421
  //and can WFS provider do ORed BBOX filters?
2422
  if ( olap.isEmpty() || ( ( mFilterCaps & ( Filter | LogicOps | BBox ) ) !=  ( Filter | LogicOps | BBox ) ) )
2423
  { //no, discard feature cache and do fresh fetch for new extent
2424
    QgsDebugMsg( QString( "Layer %1 GetRenderedOnly: fetch replace" ).arg( mLayer->name() ) );
2425
    //does original URI contain a user-specified filter?
2426
    if ( mInitUri.contains( "&FILTER=" ) )
2427
    { //yes, but FILTER & BBOX are mutually exclusive in WSF getFeature URL; merge BBOX into FILTER
2428
      setDataSourceUri( makeFilteredUri( QList<QgsRectangle>() << rect ) );
2429
    }
2430
    else
2431
    { //URI contains no FILTER; continue with stand-alone BBOX
2432
      setDataSourceUri( setUriBBox( rect ) );
2433
    }
2434
    reloadData();
2435
    if ( mValid )
2436
    { //reloadData() cannot set mGetExtent for URI with filter
2437
      mGetExtent = rect;
2438
    }
2439
  }
2440
  else if ( ( ( rect.width() - olap.width() ) < 1e-6 )
2441
            && ( ( rect.height() - olap.height() ) < 1e-6 ) )
2442
  { //difference between canvas and layer extents is less than URL BBOX precision (6 decimal places)
2443
    QgsDebugMsg( QString( "Layer %1 GetRenderedOnly: no fetch required" ).arg( mLayer->name() ) );
2444
    return;
2399 2445
  }
2400
  return ( QGis::WkbType ) i;
2446
  else
2447
  { //new canvas extent overlaps old extent: add features in new areas to existing layer
2448

  
2449
    //compute minimum expansion: at least 1/3 of desired extent in each direction that expands
2450
    QgsRectangle newGetExt( mGetExtent );
2451
    newGetExt.combineExtentWith( &rect );
2452
    double minDeltaX = ( rect.xMaximum() - rect.xMinimum() ) / 3.;
2453
    double minDeltaY = ( rect.yMaximum() - rect.yMinimum() ) / 3.;
2454
    if ( rect.xMinimum() < mGetExtent.xMinimum() && rect.xMinimum() > ( mGetExtent.xMinimum() - minDeltaX) )
2455
    {
2456
      newGetExt.setXMinimum ( mGetExtent.xMinimum() - minDeltaX );
2457
    }
2458
    if ( rect.yMinimum() < mGetExtent.yMinimum() &&  rect.yMinimum() > ( mGetExtent.yMinimum() - minDeltaY) )
2459
    {
2460
      newGetExt.setYMinimum ( mGetExtent.yMinimum() - minDeltaY );
2461
    }
2462
    if ( rect.xMaximum() > mGetExtent.xMaximum() && rect.xMaximum() < ( mGetExtent.xMaximum() + minDeltaX) )
2463
    {
2464
      newGetExt.setXMaximum ( mGetExtent.xMaximum() + minDeltaX );
2465
    }
2466
    if ( rect.yMaximum() > mGetExtent.yMaximum() && rect.yMaximum() < ( mGetExtent.yMaximum() + minDeltaY) )
2467
    {
2468
      newGetExt.setYMaximum ( mGetExtent.yMaximum() + minDeltaY );
2469
    }
2470

  
2471
    //calculate between 1 and 4 rectangles to be fetched around perimeter of existing layer extent
2472
    QList<QgsRectangle> addArea;
2473
    if ( newGetExt.xMinimum() < mGetExtent.xMinimum() )
2474
    { //new area left of current extent
2475
      addArea.append( QgsRectangle( newGetExt.xMinimum(), mGetExtent.yMinimum(), mGetExtent.xMinimum(), mGetExtent.yMaximum() ) );
2476
    }
2477
    if ( newGetExt.yMaximum() > mGetExtent.yMaximum() )
2478
    { //new area above current extent
2479
      addArea.append( QgsRectangle( newGetExt.xMinimum(), mGetExtent.yMaximum(), mGetExtent.xMaximum(), newGetExt.yMaximum() ) );
2480
    }
2481
    if ( newGetExt.xMaximum() > mGetExtent.xMaximum() )
2482
    { //new area right of current extent
2483
      addArea.append( QgsRectangle( mGetExtent.xMaximum(), mGetExtent.yMinimum(), newGetExt.xMaximum(), newGetExt.yMaximum() ) );
2484
    }
2485
    if ( newGetExt.yMinimum() < mGetExtent.yMinimum() )
2486
    { //new area beneath current extent
2487
      addArea.append( QgsRectangle( newGetExt.xMinimum(), newGetExt.yMinimum(), newGetExt.xMaximum(), mGetExtent.yMinimum() ) );
2488
    }
2489
    QgsDebugMsg( QString( "Layer %1 GetRenderedOnly: incremental fetch, %2 areas" ).arg( mLayer->name() ).arg( addArea.size() ) );
2490

  
2491
    //commented logic below allows duplicate FIDS to pass because some WFS
2492
    //providers transmit features that do not strictly intersect the BBOX
2493
    /*get WFS provider IDs of already-fetched features
2494
    QList<QgsFeatureId> edgeFids;
2495
    mWfsFids = new QSet<QString>;
2496
    for ( int i = 0; i < addArea.size() ; ++i )
2497
    { //identify features extending into each new area
2498
      edgeFids.append( mSpatialIndex->intersects( addArea[i] ) );
2499
    }
2500
    while ( ! edgeFids.isEmpty() )
2501
    { //collect known WFS provider FIDs of identified
2502
      mWfsFids->insert( mIdMap.value( edgeFids.takeFirst() ) );
2503
    }
2504
    QgsDebugMsg( QString( "WFS FIDs to discard next fetch: %1" ).arg( mWfsFids->size() ) );*/
2505
    //because of problem above, every known WFS FID is a discard candidate
2506
    mWfsFids = new QSet<QString>( mIdMap.values().toSet() );
2507

  
2508
    //save current provider extent (getFeature calls to data reader overwrite mExtent)
2509
    QgsRectangle saveMExtent( mExtent );
2510
    //fetch features in new areas, discarding features already fetched
2511
    setDataSourceUri ( makeFilteredUri( addArea ) );
2512
    if ( getFeature( dataSourceUri() ) ) //QgsWFSData::endElement discards IDs in mWfsFids
2513
    { //getFeature failed (timeout, abort); we no longer know the extent actually fetched
2514
      mValid = false;               //layer is now invalid
2515
      mGetExtent = QgsRectangle();  //getFeature extent is now unknown
2516
    }
2517
    else
2518
    {
2519
      mExtent.combineExtentWith ( &saveMExtent ); //sum of extents just received & previously cached
2520
      mGetExtent = newGetExt;
2521
    }
2522
    delete mWfsFids;
2523
    mWfsFids = 0;
2524
    //save URI for possible later user refresh()
2525
    setDataSourceUri ( setUriBBox ( rect ) );
2526
  }
2527
  QgsDebugMsg( QString( "features=%1; WSF FIDs=%2; unique WSF FIDs=%3" )
2528
                        .arg( mFeatures.size() ).arg( mIdMap.size() )
2529
                        .arg( mIdMap.values().toSet().size() ) );
2530
}
2531

  
2532
//transform provider URI to express getFeatures BBOX as FILTER
2533
QString QgsWFSProvider::makeFilteredUri( QList<QgsRectangle> addArea )
2534
{
2535
  //start with initial URI
2536
  QString uri = mInitUri;
2537
  //if the user specified a filter, get it
2538
  QDomElement root;
2539
  QDomNode n;
2540
  QDomDocument filter = QDomDocument();
2541
  QRegExp fre( "&FILTER=(.*)" );  // assumes &FILTER is last argument on URL
2542
  //does the URI already contain a valid XML filter expression?
2543
  if ( uri.contains( fre ) && filter.setContent( fre.cap(1) ) )
2544
  { //yes, create insertion point to AND a BBOX filter
2545
    root = filter.firstChild().toElement(); // <Filter> element
2546
    QDomNode userFilter = root.removeChild( root.firstChild() );
2547
    n = root.appendChild( filter.createElement( "And" ) );
2548
    n.appendChild( userFilter );
2549
    QgsDebugMsg( QString( "filter=%1" ).arg( filter.toString() ) );
2550
  }
2551
  else
2552
  { //no, create new OGC XML Filter
2553
    QDomElement root =  filter.createElement( "Filter" );
2554
    n = filter.appendChild( root );
2555
  }
2556
  //define namespace for "Box" and "coordinates"
2557
  root.setAttribute( "xmlns:gml", "http://www.opengis.net/gml" );
2558
  //OR a new BBOX clause (ver. 1.0.0) for each new area
2559
  if ( addArea.size() > 1 )
2560
  {
2561
    n = n.appendChild( filter.createElement( "Or" ) );
2562
  }
2563
  while ( ! addArea.isEmpty() )
2564
  {
2565
    QDomNode b = n.appendChild( filter.createElement( "BBOX" ) );
2566
    QDomElement p = filter.createElement("PropertyName" );
2567
    p.appendChild( filter.createTextNode( mGeometryAttribute ) );
2568
    b.appendChild( p );
2569
    //filter XML v. 1.0.0 -- accepted but ignored on some arcgis systems [wbc121130]
2570
    QDomElement v = filter.createElement( "gml:Box" );
2571
    v.setAttribute( "srsName", mSourceCRS.authid() );
2572
    QDomNode e = b.appendChild( v );
2573
    QDomNode c = e.appendChild( filter.createElement( "gml:coordinates" ) );
2574
    QgsRectangle delta = addArea.takeFirst();
2575
    c.appendChild( filter.createTextNode( QString( "%1,%2 %3,%4" )
2576
                  .arg( delta.xMinimum(), 0, 'f').arg(delta.yMinimum(), 0, 'f')
2577
                  .arg( delta.xMaximum(), 0, 'f').arg(delta.yMaximum(), 0, 'f') ) );
2578
  }
2579
  QgsDebugMsg( QString( "filter=%1" ).arg( filter.toString() ) );
2580

  
2581
  //get features contained in each new area
2582
  uri.replace( QRegExp( "BBOX=.*" ), QString( "FILTER=%1" ).arg( filter.toString( -1 ) ) );
2583
  return uri;
2401 2584
}
2402 2585

  
2586
QString QgsWFSProvider::setUriBBox( QgsRectangle rect )
2587
{
2588
  QgsDebugMsg( QString( "layer %1 GetRenderedOnly: fetching extent %2" )
2589
              .arg( mLayer->name(), rect.asWktCoordinates() ) );
2590
  QString dsURI = mInitUri;
2591
  dsURI = dsURI.replace( QRegExp( "BBOX=[^&]*" ),
2592
                          QString( "BBOX=%1,%2,%3,%4" )
2593
                          .arg( rect.xMinimum(), 0, 'f' )
2594
                          .arg( rect.yMinimum(), 0, 'f' )
2595
                          .arg( rect.xMaximum(), 0, 'f' )
2596
                          .arg( rect.yMaximum(), 0, 'f' ) );
2597
  return dsURI;
2598
}
2599

  
2600
QgsRectangle QgsWFSProvider::getUriBBox()
2601
{
2602
  bool ok = true;
2603
  QRegExp uriBBox( "&BBOX=(([-+.0-9]+,?){4})" );
2604
  if ( dataSourceUri().contains( uriBBox ) )
2605
  {
2606
    QList<QString> coords = uriBBox.cap(1).split( "," );
2607
    QList<double> coordd;
2608
    while ( ok && ! coords.isEmpty() )
2609
    {
2610
      coordd.append( coords.takeFirst().toDouble( &ok ) );
2611
    }
2612
    if ( ok )
2613
    {
2614
      return QgsRectangle( coordd[0], coordd[1], coordd[2], coordd[3] );
2615
    }
2616
  }
2617
  return QgsRectangle();
2618
}
2619
//[end issue 4280]
2620

  
2403 2621
void QgsWFSProvider::handleException( const QDomDocument& serverResponse ) const
2404 2622
{
2405 2623
  QDomElement exceptionElem = serverResponse.documentElement();
......
2435 2653
QGISEXTERN bool isProvider()
2436 2654
{
2437 2655
  return true;
2438
}
2656
}
src/providers/wfs/qgswfsprovider.h
160 160
  private:
161 161
    bool mNetworkRequestFinished;
162 162

  
163
    /**WFS Filter capabilities of interest to data provider*/
164
    enum FilterCap
165
    {
166
      /** no filter capabilities advertised */
167
      NoFilter =          0,
168
      /** ogc:Filter_Capabilities advertised */
169
      Filter =            1,
170
      /** ogc:Logical_Operators advertised */
171
      LogicOps =     1 << 1,
172
      /** ogc:BBOX advertised */
173
      BBox =         1 << 2,
174
    };
175

  
163 176
  protected:
164 177
    /**Thematic attributes*/
165 178
    QgsFieldMap mFields;
......
196 209
    int mCapabilities;
197 210
    /**GetRenderedOnly: layer asociated with this provider*/
198 211
    QgsVectorLayer *mLayer;
199
    /**GetRenderedOnly: fetch only features within canvas extent to be rendered*/
212
    /**GetRenderedOnly option flag [wbc111228 issue 4604: fetch rendered features only]*/
200 213
    bool mGetRenderedOnly;
201
    /**GetRenderedOnly initializaiton flat*/
202
    bool mInitGro;
214
    /**GetRenderedOnly initial URI*/
215
    QString mInitUri;
203 216
    /**if GetRenderedOnly, extent specified in WFS getFeatures; else empty (no constraint)*/
204 217
    QgsRectangle mGetExtent;
218
    /**incremental get: known FIDs [wbc111228 issue 4280: fetch un-cached features on demand]*/
219
    QSet<QString> *mWfsFids;
220
    /**WFS filter capabilities of interest to data provider*/
221
    int mFilterCaps;
205 222

  
206 223
    //encoding specific methods of getFeature
207 224
    int getFeatureGET( const QString& uri, const QString& geometryAttribute );
......
295 312
    void appendSupportedOperations( const QDomElement& operationsElem, int& capabilities ) const;
296 313
    /**Shows a message box with the exception string (or does nothing if the xml document is not an exception)*/
297 314
    void handleException( const QDomDocument& serverResponse ) const;
298
    /**Initializes "Cache Features" inactive processing*/
315
    /**Initialize getRenderedOnly option*/
299 316
    bool initGetRenderedOnly( QgsRectangle );
300
    /**Converts DescribeFeatureType schema geometry property type to WKBType*/
317
    /**Convert DescribeFeatureType schema geometry property type to WKBType*/
301 318
    QGis::WkbType geomTypeFromPropertyType( QString attName, QString propType );
319
    /**Fetch features from WSF provider for canvas areas to be rendered*/
320
    void incrementalGet( QgsRectangle rect );
321
    /**convert URI BBOX into filter expression*/
322
    QString makeFilteredUri( QList<QgsRectangle> addArea );
323
    /**Update BBOX parameter in GetFeature URI*/
324
    QString setUriBBox ( QgsRectangle rect );
325
    /**Extract BBOX from dataSourceUri()*/
326
    QgsRectangle getUriBBox ();
302 327

  
303 328
    void deleteData();
304 329
};
src/providers/wfs/qgswfssourceselect.cpp
59 59
  connect( btnConnect, SIGNAL( clicked() ), this, SLOT( connectToServer() ) );
60 60
  connect( btnChangeSpatialRefSys, SIGNAL( clicked() ), this, SLOT( changeCRS() ) );
61 61
  connect( treeWidget, SIGNAL( currentItemChanged( QTreeWidgetItem*, QTreeWidgetItem* ) ), this, SLOT( changeCRSFilter() ) );
62
  connect( treeWidget, SIGNAL( itemSelectionChanged() ), this, SLOT( setApplyButton() ) );
62 63
  populateConnectionList();
63 64
  mProjectionSelector = new QgsGenericProjectionSelector( this );
64 65
  mProjectionSelector->setMessage();
......
189 190
    }
190 191
    mAvailableCRS.insert( std::make_pair( featureType.name, currentCRSList ) );
191 192
  }
193
  treeWidget->resizeColumnToContents( 0 );
194
  treeWidget->sortItems( 0, Qt::AscendingOrder );
192 195

  
193 196
  if ( caps.featureTypes.count() > 0 )
194 197
  {
195
    btnAdd->setEnabled( true );
196
    treeWidget->setCurrentItem( treeWidget->topLevelItem( 0 ) );
197 198
    btnChangeSpatialRefSys->setEnabled( true );
199
    if ( caps.featureTypes.count() == 1 )
200
    { //if only one item, select it; otherwise, user must select all desired items
201
      treeWidget->setCurrentItem( treeWidget->topLevelItem( 0 ) );
202
    }
203
    if ( ! caps.canFilter )
204
    { //if WFS provider does not advertise filter capability, delete "Filter" column
205
      treeWidget->setColumnCount(4);
206
    }
198 207
  }
199 208
  else
200 209
  {
201 210
    QMessageBox::information( 0, tr( "No Layers" ), tr( "capabilities document contained no layers." ) );
211
  }
212
}
213

  
214
//enable Apply button when one or more layers are selected
215
void QgsWFSSourceSelect::setApplyButton()
216
{
217
  if ( treeWidget->selectedItems().size() )
218
  {
219
    btnAdd->setEnabled( true );
220
  }
221
  else
222
  {
202 223
    btnAdd->setEnabled( false );
203 224
  }
204 225
}
......
266 287
  QgsWFSConnection conn( cmbConnections->currentText() );
267 288
  QString pCrsString( labelCoordRefSys->text() );
268 289
  QgsCoordinateReferenceSystem pCrs( pCrsString );
269
  //prepare canvas extent info for layers with "cache features" option not set
290

  
291
  //prepare canvas extent info [wbc111228 issue 4604: fetch rendered features only]
270 292
  QgsRectangle extent;
271 293
  QVariant extentVariant = property( "MapExtent" );
272 294
  if ( extentVariant.isValid() )
......
294 316
      {
295 317
        QgsCoordinateTransform xform( pCrs, cCrs );
296 318
        extent = xform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
297
        QgsDebugMsg( QString( "canvas transform: Canvas CRS=%1, Provider CRS=%2, BBOX=%3" )
298
                     .arg( cCrs.authid(), pCrs.authid(), extent.asWktCoordinates() ) );
319
        QgsDebugMsg( QString("canvas transform: Canvas CRS=%1, Provider CRS=%2, BBOX=%3")
320
                             .arg(cCrs.authid(), pCrs.authid(), extent.asWktCoordinates() ) );
299 321
      }
300 322
    }
301 323
  }
324

  
302 325
  //create layers that user selected from this WFS source
303 326
  for ( ; sIt != selectedItems.constEnd(); ++sIt )
304 327
  { //add a wfs layer to the map
305 328
    QString typeName = ( *sIt )->text( 1 );  //WFS repository's name for layer
306 329
    QString filter = ( *sIt )->text( 4 );    //optional filter specified by user
307
    //is "cache features" checked?
308
    if (( *sIt )->checkState( 3 ) == Qt::Checked )
330
    //is "cache all features" checked?
331
    if ( ( *sIt )->checkState( 3 ) == Qt::Checked )
309 332
    { //yes: entire WFS layer will be retrieved and cached
310 333
      mUri = conn.uriGetFeature( typeName, pCrsString, filter );
311 334
    }
src/providers/wfs/qgswfssourceselect.h
70 70
    void on_btnSave_clicked();
71 71
    void on_btnLoad_clicked();
72 72
    void on_treeWidget_itemDoubleClicked( QTreeWidgetItem* item, int column );
73
    void setApplyButton();
73 74

  
74 75
    void on_buttonBox_helpRequested() { QgsContextHelp::run( metaObject()->className() ); }
75 76

  
src/ui/qgswfssourceselectbase.ui
6 6
   <rect>
7 7
    <x>0</x>
8 8
    <y>0</y>
9
    <width>592</width>
10
    <height>439</height>
9
    <width>600</width>
10
    <height>450</height>
11 11
   </rect>
12 12
  </property>
13 13
  <property name="windowTitle">
......
105 105
   </item>
106 106
   <item row="1" column="0">
107 107
    <widget class="QTreeWidget" name="treeWidget">
108
     <property name="baseSize">
109
      <size>
110
       <width>0</width>
111
       <height>0</height>
112
      </size>
113
     </property>
108 114
     <property name="selectionMode">
109 115
      <enum>QAbstractItemView::ExtendedSelection</enum>
110 116
     </property>
117
     <property name="textElideMode">
118
      <enum>Qt::ElideRight</enum>
119
     </property>
111 120
     <property name="sortingEnabled">
112 121
      <bool>true</bool>
113 122
     </property>
114 123
     <property name="columnCount">
115 124
      <number>5</number>
116 125
     </property>
126
     <attribute name="headerCascadingSectionResizes">
127
      <bool>true</bool>
128
     </attribute>
129
     <attribute name="headerDefaultSectionSize">
130
      <number>80</number>
131
     </attribute>
132
     <attribute name="headerHighlightSections">
133
      <bool>true</bool>
134
     </attribute>
117 135
     <attribute name="headerMinimumSectionSize">
118
      <number>27</number>
136
      <number>60</number>
137
     </attribute>
138
     <attribute name="headerStretchLastSection">
139
      <bool>true</bool>
140
     </attribute>
141
     <attribute name="headerCascadingSectionResizes">
142
      <bool>true</bool>
143
     </attribute>
144
     <attribute name="headerDefaultSectionSize">
145
      <number>80</number>
119 146
     </attribute>
120 147
     <attribute name="headerMinimumSectionSize">
121
      <number>27</number>
148
      <number>60</number>
149
     </attribute>
150
     <attribute name="headerHighlightSections">
151
      <bool>true</bool>
152
     </attribute>
153
     <attribute name="headerStretchLastSection">
154
      <bool>true</bool>
122 155
     </attribute>
123 156
     <column>
124 157
      <property name="text">
......
137 170
     </column>
138 171
     <column>
139 172
      <property name="text">
140
       <string>Cache
173
       <string>Cache All
141 174
Features</string>
142 175
      </property>
176
      <property name="toolTip">
177
       <string extracomment="Retrieve all features from WFS repository when layer is created."/>
178
      </property>
179
      <property name="statusTip">
180
       <string extracomment="Caution: &lt;b&gt;slow&lt;/b&gt; layer creation from large remote WFS providers!"/>
181
      </property>
143 182
      <property name="textAlignment">
144
       <set>AlignHCenter|AlignVCenter|AlignCenter</set>
183
       <set>AlignLeft|AlignVCenter</set>
145 184
      </property>
146 185
     </column>
147 186
     <column>
148 187
      <property name="text">
149
       <string>Filter</string>
188
       <string>Filter (double click to enter)</string>
150 189
      </property>
151 190
     </column>
152 191
    </widget>