QGIS API Documentation  2.99.0-Master (11bb234)
qgssearchquerybuilder.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgssearchquerybuilder.cpp - Query builder for search strings
3  ----------------------
4  begin : March 2006
5  copyright : (C) 2006 by Martin Dobias
6  email : wonder.sk at gmail dot 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 <QDomDocument>
17 #include <QDomElement>
18 #include <QFileDialog>
19 #include <QFileInfo>
20 #include <QInputDialog>
21 #include <QListView>
22 #include <QMessageBox>
23 #include <QStandardItem>
24 #include <QTextStream>
25 
26 #include "qgssettings.h"
27 #include "qgsfeature.h"
28 #include "qgsfeatureiterator.h"
29 #include "qgsfields.h"
30 #include "qgssearchquerybuilder.h"
31 #include "qgsexpression.h"
32 #include "qgsvectorlayer.h"
33 #include "qgslogger.h"
34 
36  QWidget *parent, Qt::WindowFlags fl )
37  : QDialog( parent, fl )
38  , mLayer( layer )
39 {
40  setupUi( this );
41  setupListViews();
42 
43  setWindowTitle( tr( "Search query builder" ) );
44 
45  QPushButton *pbn = new QPushButton( tr( "&Test" ) );
46  buttonBox->addButton( pbn, QDialogButtonBox::ActionRole );
47  connect( pbn, &QAbstractButton::clicked, this, &QgsSearchQueryBuilder::on_btnTest_clicked );
48 
49  pbn = new QPushButton( tr( "&Clear" ) );
50  buttonBox->addButton( pbn, QDialogButtonBox::ActionRole );
51  connect( pbn, &QAbstractButton::clicked, this, &QgsSearchQueryBuilder::on_btnClear_clicked );
52 
53  pbn = new QPushButton( tr( "&Save..." ) );
54  buttonBox->addButton( pbn, QDialogButtonBox::ActionRole );
55  pbn->setToolTip( tr( "Save query to an xml file" ) );
56  connect( pbn, &QAbstractButton::clicked, this, &QgsSearchQueryBuilder::saveQuery );
57 
58  pbn = new QPushButton( tr( "&Load..." ) );
59  buttonBox->addButton( pbn, QDialogButtonBox::ActionRole );
60  pbn->setToolTip( tr( "Load query from xml file" ) );
61  connect( pbn, &QAbstractButton::clicked, this, &QgsSearchQueryBuilder::loadQuery );
62 
63  if ( layer )
64  lblDataUri->setText( layer->name() );
65  populateFields();
66 }
67 
68 void QgsSearchQueryBuilder::populateFields()
69 {
70  if ( !mLayer )
71  return;
72 
73  const QgsFields &fields = mLayer->fields();
74  for ( int idx = 0; idx < fields.count(); ++idx )
75  {
76  QString fieldName = fields.at( idx ).name();
77  mFieldMap[fieldName] = idx;
78  QStandardItem *myItem = new QStandardItem( fieldName );
79  myItem->setEditable( false );
80  mModelFields->insertRow( mModelFields->rowCount(), myItem );
81  }
82 }
83 
84 void QgsSearchQueryBuilder::setupListViews()
85 {
86  //Models
87  mModelFields = new QStandardItemModel();
88  mModelValues = new QStandardItemModel();
89  lstFields->setModel( mModelFields );
90  lstValues->setModel( mModelValues );
91  // Modes
92  lstFields->setViewMode( QListView::ListMode );
93  lstValues->setViewMode( QListView::ListMode );
94  lstFields->setSelectionBehavior( QAbstractItemView::SelectRows );
95  lstValues->setSelectionBehavior( QAbstractItemView::SelectRows );
96  // Performance tip since Qt 4.1
97  lstFields->setUniformItemSizes( true );
98  lstValues->setUniformItemSizes( true );
99 }
100 
101 void QgsSearchQueryBuilder::getFieldValues( int limit )
102 {
103  if ( !mLayer )
104  {
105  return;
106  }
107  // clear the values list
108  mModelValues->clear();
109 
110  // determine the field type
111  QString fieldName = mModelFields->data( lstFields->currentIndex() ).toString();
112  int fieldIndex = mFieldMap[fieldName];
113  QgsField field = mLayer->fields().at( fieldIndex );//provider->fields().at( fieldIndex );
114  bool numeric = ( field.type() == QVariant::Int || field.type() == QVariant::Double );
115 
116  QgsFeature feat;
117  QString value;
118 
119  QgsAttributeList attrs;
120  attrs.append( fieldIndex );
121 
122  QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( attrs ) );
123 
124  lstValues->setCursor( Qt::WaitCursor );
125  // Block for better performance
126  mModelValues->blockSignals( true );
127  lstValues->setUpdatesEnabled( false );
128 
129  // MH: keep already inserted values in a set. Querying is much faster compared to QStandardItemModel::findItems
130  QSet<QString> insertedValues;
131 
132  while ( fit.nextFeature( feat ) &&
133  ( limit == 0 || mModelValues->rowCount() != limit ) )
134  {
135  value = feat.attribute( fieldIndex ).toString();
136 
137  if ( !numeric )
138  {
139  // put string in single quotes and escape single quotes in the string
140  value = '\'' + value.replace( '\'', QLatin1String( "''" ) ) + '\'';
141  }
142 
143  // add item only if it's not there already
144  if ( !insertedValues.contains( value ) )
145  {
146  QStandardItem *myItem = new QStandardItem( value );
147  myItem->setEditable( false );
148  mModelValues->insertRow( mModelValues->rowCount(), myItem );
149  insertedValues.insert( value );
150  }
151  }
152  // Unblock for normal use
153  mModelValues->blockSignals( false );
154  lstValues->setUpdatesEnabled( true );
155  // TODO: already sorted, signal emit to refresh model
156  mModelValues->sort( 0 );
157  lstValues->setCursor( Qt::ArrowCursor );
158 }
159 
161 {
162  getFieldValues( 25 );
163 }
164 
166 {
167  getFieldValues( 0 );
168 }
169 
171 {
172  long count = countRecords( txtSQL->text() );
173 
174  // error?
175  if ( count == -1 )
176  return;
177 
178  QMessageBox::information( this, tr( "Search results" ), tr( "Found %n matching feature(s).", "test result", count ) );
179 }
180 
181 // This method tests the number of records that would be returned
182 long QgsSearchQueryBuilder::countRecords( const QString &searchString )
183 {
184  QgsExpression search( searchString );
185  if ( search.hasParserError() )
186  {
187  QMessageBox::critical( this, tr( "Search string parsing error" ), search.parserErrorString() );
188  return -1;
189  }
190 
191  if ( !mLayer )
192  return -1;
193 
194  bool fetchGeom = search.needsGeometry();
195 
196  int count = 0;
197  QgsFeature feat;
198 
200 
201  if ( !search.prepare( &context ) )
202  {
203  QMessageBox::critical( this, tr( "Evaluation error" ), search.evalErrorString() );
204  return -1;
205  }
206 
207  QApplication::setOverrideCursor( Qt::WaitCursor );
208 
210 
211  while ( fit.nextFeature( feat ) )
212  {
213  context.setFeature( feat );
214  QVariant value = search.evaluate( &context );
215  if ( value.toInt() != 0 )
216  {
217  count++;
218  }
219 
220  // check if there were errors during evaulating
221  if ( search.hasEvalError() )
222  break;
223  }
224 
225  QApplication::restoreOverrideCursor();
226 
227  if ( search.hasEvalError() )
228  {
229  QMessageBox::critical( this, tr( "Error during search" ), search.evalErrorString() );
230  return -1;
231  }
232 
233  return count;
234 }
235 
236 
238 {
239  // if user hits Ok and there is no query, skip the validation
240  if ( txtSQL->text().trimmed().length() > 0 )
241  {
242  accept();
243  return;
244  }
245 
246  // test the query to see if it will result in a valid layer
247  long numRecs = countRecords( txtSQL->text() );
248  if ( numRecs == -1 )
249  {
250  // error shown in countRecords
251  }
252  else if ( numRecs == 0 )
253  {
254  QMessageBox::warning( this, tr( "No Records" ), tr( "The query you specified results in zero records being returned." ) );
255  }
256  else
257  {
258  accept();
259  }
260 
261 }
262 
264 {
265  txtSQL->insertText( QStringLiteral( " = " ) );
266 }
267 
269 {
270  txtSQL->insertText( QStringLiteral( " < " ) );
271 }
272 
274 {
275  txtSQL->insertText( QStringLiteral( " > " ) );
276 }
277 
279 {
280  txtSQL->insertText( QStringLiteral( "%" ) );
281 }
282 
284 {
285  txtSQL->insertText( QStringLiteral( " IN " ) );
286 }
287 
289 {
290  txtSQL->insertText( QStringLiteral( " NOT IN " ) );
291 }
292 
294 {
295  txtSQL->insertText( QStringLiteral( " LIKE " ) );
296 }
297 
299 {
300  return txtSQL->text();
301 }
302 
304 {
305  txtSQL->setText( searchString );
306 }
307 
309 {
310  txtSQL->insertText( QgsExpression::quotedColumnRef( mModelFields->data( index ).toString() ) );
311 }
312 
314 {
315  txtSQL->insertText( mModelValues->data( index ).toString() );
316 }
317 
319 {
320  txtSQL->insertText( QStringLiteral( " <= " ) );
321 }
322 
324 {
325  txtSQL->insertText( QStringLiteral( " >= " ) );
326 }
327 
329 {
330  txtSQL->insertText( QStringLiteral( " != " ) );
331 }
332 
334 {
335  txtSQL->insertText( QStringLiteral( " AND " ) );
336 }
337 
339 {
340  txtSQL->insertText( QStringLiteral( " NOT " ) );
341 }
342 
344 {
345  txtSQL->insertText( QStringLiteral( " OR " ) );
346 }
347 
349 {
350  txtSQL->clear();
351 }
352 
354 {
355  txtSQL->insertText( QStringLiteral( " ILIKE " ) );
356 }
357 
359 {
360  QgsSettings s;
361  QString lastQueryFileDir = s.value( QStringLiteral( "/UI/lastQueryFileDir" ), QDir::homePath() ).toString();
362  //save as qqt (QGIS query file)
363  QString saveFileName = QFileDialog::getSaveFileName( nullptr, tr( "Save query to file" ), lastQueryFileDir, QStringLiteral( "*.qqf" ) );
364  if ( saveFileName.isNull() )
365  {
366  return;
367  }
368 
369  if ( !saveFileName.endsWith( QLatin1String( ".qqf" ), Qt::CaseInsensitive ) )
370  {
371  saveFileName += QLatin1String( ".qqf" );
372  }
373 
374  QFile saveFile( saveFileName );
375  if ( !saveFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
376  {
377  QMessageBox::critical( nullptr, tr( "Error" ), tr( "Could not open file for writing" ) );
378  return;
379  }
380 
381  QDomDocument xmlDoc;
382  QDomElement queryElem = xmlDoc.createElement( QStringLiteral( "Query" ) );
383  QDomText queryTextNode = xmlDoc.createTextNode( txtSQL->text() );
384  queryElem.appendChild( queryTextNode );
385  xmlDoc.appendChild( queryElem );
386 
387  QTextStream fileStream( &saveFile );
388  xmlDoc.save( fileStream, 2 );
389 
390  QFileInfo fi( saveFile );
391  s.setValue( QStringLiteral( "/UI/lastQueryFileDir" ), fi.absolutePath() );
392 }
393 
395 {
396  QgsSettings s;
397  QString lastQueryFileDir = s.value( QStringLiteral( "/UI/lastQueryFileDir" ), QDir::homePath() ).toString();
398 
399  QString queryFileName = QFileDialog::getOpenFileName( nullptr, tr( "Load query from file" ), lastQueryFileDir, tr( "Query files" ) + " (*.qqf);;" + tr( "All files" ) + " (*)" );
400  if ( queryFileName.isNull() )
401  {
402  return;
403  }
404 
405  QFile queryFile( queryFileName );
406  if ( !queryFile.open( QIODevice::ReadOnly ) )
407  {
408  QMessageBox::critical( nullptr, tr( "Error" ), tr( "Could not open file for reading" ) );
409  return;
410  }
411  QDomDocument queryDoc;
412  if ( !queryDoc.setContent( &queryFile ) )
413  {
414  QMessageBox::critical( nullptr, tr( "Error" ), tr( "File is not a valid xml document" ) );
415  return;
416  }
417 
418  QDomElement queryElem = queryDoc.firstChildElement( QStringLiteral( "Query" ) );
419  if ( queryElem.isNull() )
420  {
421  QMessageBox::critical( nullptr, tr( "Error" ), tr( "File is not a valid query document" ) );
422  return;
423  }
424 
425  QString query = queryElem.text();
426 
427  //todo: test if all the attributes are valid
428  QgsExpression search( query );
429  if ( search.hasParserError() )
430  {
431  QMessageBox::critical( this, tr( "Search string parsing error" ), search.parserErrorString() );
432  return;
433  }
434 
435  QString newQueryText = query;
436 
437 #if 0
438  // TODO: implement with visitor pattern in QgsExpression
439 
440  QStringList attributes = searchTree->referencedColumns();
441  QMap< QString, QString> attributesToReplace;
442  QStringList existingAttributes;
443 
444  //get all existing fields
445  QMap<QString, int>::const_iterator fieldIt = mFieldMap.constBegin();
446  for ( ; fieldIt != mFieldMap.constEnd(); ++fieldIt )
447  {
448  existingAttributes.push_back( fieldIt.key() );
449  }
450 
451  //if a field does not exist, ask what field should be used instead
452  QStringList::const_iterator attIt = attributes.constBegin();
453  for ( ; attIt != attributes.constEnd(); ++attIt )
454  {
455  //test if attribute is there
456  if ( !mFieldMap.contains( *attIt ) )
457  {
458  bool ok;
459  QString replaceAttribute = QInputDialog::getItem( 0, tr( "Select attribute" ), tr( "There is no attribute '%1' in the current vector layer. Please select an existing attribute" ).arg( *attIt ),
460  existingAttributes, 0, false, &ok );
461  if ( !ok || replaceAttribute.isEmpty() )
462  {
463  return;
464  }
465  attributesToReplace.insert( *attIt, replaceAttribute );
466  }
467  }
468 
469  //Now replace all the string in the query
470  QList<QgsSearchTreeNode *> columnRefList = searchTree->columnRefNodes();
471  QList<QgsSearchTreeNode *>::iterator columnIt = columnRefList.begin();
472  for ( ; columnIt != columnRefList.end(); ++columnIt )
473  {
474  QMap< QString, QString>::const_iterator replaceIt = attributesToReplace.find( ( *columnIt )->columnRef() );
475  if ( replaceIt != attributesToReplace.constEnd() )
476  {
477  ( *columnIt )->setColumnRef( replaceIt.value() );
478  }
479  }
480 
481  if ( attributesToReplace.size() > 0 )
482  {
483  newQueryText = query;
484  }
485 #endif
486 
487  txtSQL->clear();
488  txtSQL->insertText( newQueryText );
489 }
490 
Class for parsing and evaluation of expressions (formerly called "search strings").
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
QgsSearchQueryBuilder(QgsVectorLayer *layer, QWidget *parent=nullptr, Qt::WindowFlags fl=QgisGui::ModalDialogFlags)
Constructor - takes pointer to vector layer as a parameter.
Wrapper for iterator of features from vector data provider or vector layer.
static unsigned index
static QString quotedColumnRef(QString name)
Returns a quoted column reference (in double quotes)
QString name
Definition: qgsfield.h:53
This class is a composition of two QSettings instances:
Definition: qgssettings.h:54
void on_lstFields_doubleClicked(const QModelIndex &index)
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
void setSearchString(const QString &searchString)
change search string shown in text field
QVariant evaluate()
Evaluate the feature and return the result.
QString evalErrorString() const
Returns evaluation error.
Container of fields for a vector layer.
Definition: qgsfields.h:39
bool needsGeometry() const
Returns true if the expression uses feature geometry for some computation.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:61
int count() const
Return number of items.
Definition: qgsfields.cpp:115
QString parserErrorString() const
Returns parser error.
QgsFields fields() const
Returns the list of fields of this layer.
QgsField at(int i) const
Get field at particular index (must be in range 0..N-1)
Definition: qgsfields.cpp:135
void setValue(const QString &key, const QVariant &value, const QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
void on_btnSampleValues_clicked()
Get sample distinct values for the selected field.
void on_btnGetAllValues_clicked()
Get all distinct values for the field.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const
Query the layer for features specified in request.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
QList< int > QgsAttributeList
void on_btnTest_clicked()
Test the constructed search string to see if it&#39;s correct.
static QList< QgsExpressionContextScope * > globalProjectLayerScopes(const QgsMapLayer *layer)
Creates a list of three scopes: global, layer&#39;s project and layer.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:45
void on_lstValues_doubleClicked(const QModelIndex &index)
bool prepare(const QgsExpressionContext *context)
Get the expression ready for evaluation - find out column indexes.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), const Section section=NoSection) const
Returns the value for setting key.
QString name
Definition: qgsmaplayer.h:57
bool nextFeature(QgsFeature &f)
Geometry is not required. It may still be returned if e.g. required for a filter condition.
bool hasEvalError() const
Returns true if an error occurred when evaluating last input.
Represents a vector layer which manages a vector based data sets.
QVariant::Type type() const
Gets variant type of the field as it will be retrieved from data source.
Definition: qgsfield.cpp:94
QVariant attribute(const QString &name) const
Lookup attribute value from attribute name.
Definition: qgsfeature.cpp:255
QString searchString()
returns newly created search string