1 /***************************************************************************
2                         qgsinbuiltlocatorfilters.cpp
3                         ----------------------------
4    begin                : May 2017
5    copyright            : (C) 2017 by Nyall Dawson
6    email                : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include <QClipboard>
19 #include <QMap>
20 #include <QSpinBox>
21 #include <QString>
22 #include <QToolButton>
23 #include <QUrl>
24 
25 #include "qgsapplication.h"
26 #include "qgscoordinatereferencesystem.h"
27 #include "qgscoordinatetransform.h"
28 #include "qgscoordinateutils.h"
29 #include "qgsinbuiltlocatorfilters.h"
30 #include "qgsproject.h"
31 #include "qgslayertree.h"
32 #include "qgsfeedback.h"
33 #include "qgisapp.h"
34 #include "qgsmaplayermodel.h"
35 #include "qgslayoutmanager.h"
36 #include "qgsmapcanvas.h"
37 #include "qgsfeatureaction.h"
38 #include "qgsvectorlayerfeatureiterator.h"
39 #include "qgsexpressioncontextutils.h"
40 #include "qgssettings.h"
41 #include "qgsunittypes.h"
42 #include "qgslocatorwidget.h"
43 
44 
QgsLayerTreeLocatorFilter(QObject * parent)45 QgsLayerTreeLocatorFilter::QgsLayerTreeLocatorFilter( QObject *parent )
46   : QgsLocatorFilter( parent )
47 {}
48 
clone() const49 QgsLayerTreeLocatorFilter *QgsLayerTreeLocatorFilter::clone() const
50 {
51   return new QgsLayerTreeLocatorFilter();
52 }
53 
fetchResults(const QString & string,const QgsLocatorContext & context,QgsFeedback *)54 void QgsLayerTreeLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback * )
55 {
56   QgsLayerTree *tree = QgsProject::instance()->layerTreeRoot();
57   const QList<QgsLayerTreeLayer *> layers = tree->findLayers();
58   for ( QgsLayerTreeLayer *layer : layers )
59   {
60     // if the layer is broken, don't include it in the results
61     if ( ! layer->layer() )
62       continue;
63 
64     QgsLocatorResult result;
65     result.displayString = layer->layer()->name();
66     result.userData = layer->layerId();
67     result.icon = QgsMapLayerModel::iconForLayer( layer->layer() );
68 
69     // return all the layers in case the string query is empty using an equal default score
70     if ( context.usingPrefix && string.isEmpty() )
71     {
72       emit resultFetched( result );
73       continue;
74     }
75 
76     result.score = fuzzyScore( result.displayString, string );
77 
78     if ( result.score > 0 )
79       emit resultFetched( result );
80   }
81 }
82 
triggerResult(const QgsLocatorResult & result)83 void QgsLayerTreeLocatorFilter::triggerResult( const QgsLocatorResult &result )
84 {
85   QString layerId = result.userData.toString();
86   QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId );
87   QgisApp::instance()->setActiveLayer( layer );
88 }
89 
90 //
91 // QgsLayoutLocatorFilter
92 //
93 
QgsLayoutLocatorFilter(QObject * parent)94 QgsLayoutLocatorFilter::QgsLayoutLocatorFilter( QObject *parent )
95   : QgsLocatorFilter( parent )
96 {}
97 
clone() const98 QgsLayoutLocatorFilter *QgsLayoutLocatorFilter::clone() const
99 {
100   return new QgsLayoutLocatorFilter();
101 }
102 
fetchResults(const QString & string,const QgsLocatorContext & context,QgsFeedback *)103 void QgsLayoutLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback * )
104 {
105   const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts();
106   for ( QgsMasterLayoutInterface *layout : layouts )
107   {
108     // if the layout is broken, don't include it in the results
109     if ( ! layout )
110       continue;
111 
112     QgsLocatorResult result;
113     result.displayString = layout->name();
114     result.userData = layout->name();
115 
116     if ( context.usingPrefix && string.isEmpty() )
117     {
118       emit resultFetched( result );
119       continue;
120     }
121 
122     result.score = fuzzyScore( result.displayString, string );
123 
124     if ( result.score > 0 )
125       emit resultFetched( result );
126   }
127 }
128 
triggerResult(const QgsLocatorResult & result)129 void QgsLayoutLocatorFilter::triggerResult( const QgsLocatorResult &result )
130 {
131   QString layoutName = result.userData.toString();
132   QgsMasterLayoutInterface *layout = QgsProject::instance()->layoutManager()->layoutByName( layoutName );
133   if ( !layout )
134     return;
135 
136   QgisApp::instance()->openLayoutDesignerDialog( layout );
137 }
138 
QgsActionLocatorFilter(const QList<QWidget * > & parentObjectsForActions,QObject * parent)139 QgsActionLocatorFilter::QgsActionLocatorFilter( const QList<QWidget *> &parentObjectsForActions, QObject *parent )
140   : QgsLocatorFilter( parent )
141   , mActionParents( parentObjectsForActions )
142 {
143   setUseWithoutPrefix( false );
144 }
145 
clone() const146 QgsActionLocatorFilter *QgsActionLocatorFilter::clone() const
147 {
148   return new QgsActionLocatorFilter( mActionParents );
149 }
150 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback *)151 void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback * )
152 {
153   // collect results in main thread, since this method is inexpensive and
154   // accessing the gui actions is not thread safe
155 
156   QList<QAction *> found;
157 
158   for ( QWidget *object : qgis::as_const( mActionParents ) )
159   {
160     searchActions( string, object, found );
161   }
162 }
163 
triggerResult(const QgsLocatorResult & result)164 void QgsActionLocatorFilter::triggerResult( const QgsLocatorResult &result )
165 {
166   QAction *action = qobject_cast< QAction * >( qvariant_cast<QObject *>( result.userData ) );
167   if ( action )
168     action->trigger();
169 }
170 
searchActions(const QString & string,QWidget * parent,QList<QAction * > & found)171 void QgsActionLocatorFilter::searchActions( const QString &string, QWidget *parent, QList<QAction *> &found )
172 {
173   const QList< QWidget *> children = parent->findChildren<QWidget *>();
174   for ( QWidget *widget : children )
175   {
176     searchActions( string, widget, found );
177   }
178 
179   QRegularExpression extractFromTooltip( QStringLiteral( "<b>(.*)</b>" ) );
180   QRegularExpression newLineToSpace( QStringLiteral( "[\\s\\n\\r]+" ) );
181 
182   const auto constActions = parent->actions();
183   for ( QAction *action : constActions )
184   {
185     if ( action->menu() )
186     {
187       searchActions( string, action->menu(), found );
188       continue;
189     }
190 
191     if ( !action->isEnabled() || !action->isVisible() || action->text().isEmpty() )
192       continue;
193     if ( found.contains( action ) )
194       continue;
195 
196     QString searchText = action->text();
197     searchText.replace( '&', QString() );
198 
199     QString tooltip = action->toolTip();
200     tooltip.replace( newLineToSpace, QStringLiteral( " " ) );
201     QRegularExpressionMatch match = extractFromTooltip.match( tooltip );
202     if ( match.hasMatch() )
203     {
204       tooltip = match.captured( 1 );
205     }
206     tooltip.replace( QLatin1String( "..." ), QString() );
207     tooltip.replace( QString( QChar( 0x2026 ) ), QString() );
208     searchText.replace( QLatin1String( "..." ), QString() );
209     searchText.replace( QString( QChar( 0x2026 ) ), QString() );
210     bool uniqueTooltip = searchText.trimmed().compare( tooltip.trimmed(), Qt::CaseInsensitive ) != 0;
211     if ( action->isChecked() )
212     {
213       searchText += QStringLiteral( " [%1]" ).arg( tr( "Active" ) );
214     }
215     if ( uniqueTooltip )
216     {
217       searchText += QStringLiteral( " (%1)" ).arg( tooltip.trimmed() );
218     }
219 
220     QgsLocatorResult result;
221     result.displayString = searchText;
222     result.userData = QVariant::fromValue( action );
223     result.icon = action->icon();
224     result.score = fuzzyScore( result.displayString, string );
225 
226     if ( result.score > 0 )
227     {
228       found << action;
229       emit resultFetched( result );
230     }
231   }
232 }
233 
234 //
235 // QgsActiveLayerFeaturesLocatorFilter
236 //
237 
QgsActiveLayerFeaturesLocatorFilter(QObject * parent)238 QgsActiveLayerFeaturesLocatorFilter::QgsActiveLayerFeaturesLocatorFilter( QObject *parent )
239   : QgsLocatorFilter( parent )
240 {
241   setUseWithoutPrefix( false );
242 }
243 
clone() const244 QgsActiveLayerFeaturesLocatorFilter *QgsActiveLayerFeaturesLocatorFilter::clone() const
245 {
246   return new QgsActiveLayerFeaturesLocatorFilter();
247 }
248 
fieldRestriction(QString & searchString,bool * isRestricting)249 QString QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( QString &searchString, bool *isRestricting )
250 {
251   QString _fieldRestriction;
252   searchString = searchString.trimmed();
253   if ( isRestricting )
254     *isRestricting = searchString.startsWith( '@' );
255   if ( searchString.startsWith( '@' ) )
256   {
257     _fieldRestriction = searchString.left( std::min( searchString.indexOf( ' ' ), searchString.length() ) ).remove( 0, 1 );
258     searchString = searchString.mid( _fieldRestriction.length() + 2 );
259   }
260   return _fieldRestriction;
261 }
262 
prepare(const QString & string,const QgsLocatorContext & context)263 QStringList QgsActiveLayerFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
264 {
265   mFieldsCompletion.clear();
266 
267   // Normally skip very short search strings, unless when specifically searching using this filter or try to match fields
268   if ( string.length() < 3 && !context.usingPrefix && !string.startsWith( '@' ) )
269     return QStringList();
270 
271   QgsSettings settings;
272   mMaxTotalResults = settings.value( QStringLiteral( "locator_filters/active_layer_features/limit_global" ), 30, QgsSettings::App ).toInt();
273 
274   QgsVectorLayer *layer = qobject_cast< QgsVectorLayer *>( QgisApp::instance()->activeLayer() );
275   if ( !layer )
276     return QStringList();
277 
278   mLayerIsSpatial = layer->isSpatial();
279 
280   mDispExpression = QgsExpression( layer->displayExpression() );
281   mContext.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
282   mDispExpression.prepare( &mContext );
283 
284   // determine if search is restricted to a specific field
285   QString searchString = string;
286   bool isRestricting = false;
287   QString _fieldRestriction = fieldRestriction( searchString, &isRestricting );
288   bool allowNumeric = false;
289   double numericalValue = searchString.toDouble( &allowNumeric );
290 
291   // search in display expression if no field restriction
292   if ( !isRestricting )
293   {
294     QgsFeatureRequest req;
295     req.setSubsetOfAttributes( qgis::setToList( mDispExpression.referencedAttributeIndexes( layer->fields() ) ) );
296     if ( !mDispExpression.needsGeometry() )
297       req.setFlags( QgsFeatureRequest::NoGeometry );
298     QString enhancedSearch = searchString;
299     enhancedSearch.replace( ' ', '%' );
300     req.setFilterExpression( QStringLiteral( "%1 ILIKE '%%2%'" )
301                              .arg( layer->displayExpression(), enhancedSearch ) );
302     req.setLimit( mMaxTotalResults );
303     mDisplayTitleIterator = layer->getFeatures( req );
304   }
305   else
306   {
307     mDisplayTitleIterator = QgsFeatureIterator();
308   }
309 
310   // build up request expression
311   QStringList expressionParts;
312   QStringList completionList;
313   const QgsFields fields = layer->fields();
314   QgsAttributeList subsetOfAttributes = qgis::setToList( mDispExpression.referencedAttributeIndexes( layer->fields() ) );
315   for ( const QgsField &field : fields )
316   {
317     if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::NotSearchable ) )
318       continue;
319 
320     if ( isRestricting && !field.name().startsWith( _fieldRestriction ) )
321       continue;
322 
323     if ( isRestricting )
324     {
325       int index = layer->fields().indexFromName( field.name() );
326       if ( !subsetOfAttributes.contains( index ) )
327         subsetOfAttributes << index;
328     }
329 
330     // if we are trying to find a field (and not searching anything yet)
331     // keep the list of matching fields to display them as results
332     if ( isRestricting && searchString.isEmpty() && _fieldRestriction != field.name() )
333     {
334       mFieldsCompletion << field.name();
335     }
336 
337     // the completion list (returned by the current method) is used by the locator line edit directly
338     completionList.append( QStringLiteral( "@%1 " ).arg( field.name() ) );
339 
340     if ( field.type() == QVariant::String )
341     {
342       expressionParts << QStringLiteral( "%1 ILIKE '%%2%'" ).arg( QgsExpression::quotedColumnRef( field.name() ),
343                       searchString );
344     }
345     else if ( allowNumeric && field.isNumeric() )
346     {
347       expressionParts << QStringLiteral( "%1 = %2" ).arg( QgsExpression::quotedColumnRef( field.name() ), QString::number( numericalValue, 'g', 17 ) );
348     }
349   }
350 
351   QString expression = QStringLiteral( "(%1)" ).arg( expressionParts.join( QLatin1String( " ) OR ( " ) ) );
352 
353   QgsFeatureRequest req;
354   if ( !mDispExpression.needsGeometry() )
355     req.setFlags( QgsFeatureRequest::NoGeometry );
356   req.setFilterExpression( expression );
357   if ( isRestricting )
358     req.setSubsetOfAttributes( subsetOfAttributes );
359 
360   req.setLimit( mMaxTotalResults );
361   mFieldIterator = layer->getFeatures( req );
362 
363   mLayerId = layer->id();
364   mLayerIcon = QgsMapLayerModel::iconForLayer( layer );
365   mAttributeAliases.clear();
366   for ( int idx = 0; idx < layer->fields().size(); ++idx )
367   {
368     mAttributeAliases.append( layer->attributeDisplayName( idx ) );
369   }
370 
371   return completionList;
372 }
373 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback * feedback)374 void QgsActiveLayerFeaturesLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
375 {
376   QgsFeatureIds featuresFound;
377   QgsFeature f;
378   QString searchString = string;
379   fieldRestriction( searchString );
380 
381   // propose available fields for restriction
382   for ( const QString &field : qgis::as_const( mFieldsCompletion ) )
383   {
384     QgsLocatorResult result;
385     result.displayString = QStringLiteral( "@%1" ).arg( field );
386     result.description = tr( "Limit the search to the field '%1'" ).arg( field );
387     result.userData = QVariantMap( {{QStringLiteral( "type" ), QVariant::fromValue( ResultType::FieldRestriction )},
388       {QStringLiteral( "search_text" ), QStringLiteral( "%1 @%2 " ).arg( prefix(), field ) } } );
389     result.score = 1;
390     emit resultFetched( result );
391   }
392 
393   // search in display title
394   if ( mDisplayTitleIterator.isValid() )
395   {
396     while ( mDisplayTitleIterator.nextFeature( f ) )
397     {
398       if ( feedback->isCanceled() )
399         return;
400 
401       mContext.setFeature( f );
402 
403       QgsLocatorResult result;
404 
405       result.displayString =  mDispExpression.evaluate( &mContext ).toString();
406       result.userData = QVariantMap(
407       {
408         {QStringLiteral( "type" ), QVariant::fromValue( ResultType::Feature )},
409         {QStringLiteral( "feature_id" ), f.id()},
410         {QStringLiteral( "layer_id" ), mLayerId},
411         {QStringLiteral( "layer_is_spatial" ), mLayerIsSpatial}
412       } );
413       result.icon = mLayerIcon;
414       result.score = static_cast< double >( searchString.length() ) / result.displayString.size();
415       if ( mLayerIsSpatial )
416         result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
417 
418       emit resultFetched( result );
419 
420       featuresFound << f.id();
421 
422       if ( featuresFound.count() >= mMaxTotalResults )
423         break;
424     }
425   }
426 
427   // search in fields
428   while ( mFieldIterator.nextFeature( f ) )
429   {
430     if ( feedback->isCanceled() )
431       return;
432 
433     // do not display twice the same feature
434     if ( featuresFound.contains( f.id() ) )
435       continue;
436 
437     QgsLocatorResult result;
438 
439     mContext.setFeature( f );
440 
441     // find matching field content
442     int idx = 0;
443     const QgsAttributes attributes = f.attributes();
444     for ( const QVariant &var : attributes )
445     {
446       QString attrString = var.toString();
447       if ( attrString.contains( searchString, Qt::CaseInsensitive ) )
448       {
449         if ( idx < mAttributeAliases.count() )
450           result.displayString = QStringLiteral( "%1 (%2)" ).arg( attrString, mAttributeAliases[idx] );
451         else
452           result.displayString = attrString;
453         break;
454       }
455       idx++;
456     }
457     if ( result.displayString.isEmpty() )
458       continue; //not sure how this result slipped through...
459 
460     result.description = mDispExpression.evaluate( &mContext ).toString();
461     result.userData = QVariantMap(
462     {
463       {QStringLiteral( "type" ), QVariant::fromValue( ResultType::Feature )},
464       {QStringLiteral( "feature_id" ), f.id()},
465       {QStringLiteral( "layer_id" ), mLayerId},
466       {QStringLiteral( "layer_is_spatial" ), mLayerIsSpatial}
467     } );
468     result.icon = mLayerIcon;
469     result.score = static_cast< double >( searchString.length() ) / result.displayString.size();
470     if ( mLayerIsSpatial )
471       result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
472 
473     emit resultFetched( result );
474 
475     featuresFound << f.id();
476     if ( featuresFound.count() >= mMaxTotalResults )
477       break;
478   }
479 }
480 
triggerResult(const QgsLocatorResult & result)481 void QgsActiveLayerFeaturesLocatorFilter::triggerResult( const QgsLocatorResult &result )
482 {
483   triggerResultFromAction( result, NoEntry );
484 }
485 
triggerResultFromAction(const QgsLocatorResult & result,const int actionId)486 void QgsActiveLayerFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId )
487 {
488   QVariantMap data = result.userData.value<QVariantMap>();
489   switch ( data.value( QStringLiteral( "type" ) ).value<ResultType>() )
490   {
491     case ResultType::Feature:
492     {
493       QgsVectorLayer *layer = QgsProject::instance()->mapLayer<QgsVectorLayer *>( data.value( QStringLiteral( "layer_id" ) ).toString() );
494       if ( layer )
495       {
496         QgsFeatureId fid = data.value( QStringLiteral( "feature_id" ) ).value<QgsFeatureId>();
497         if ( actionId == OpenForm || !data.value( QStringLiteral( "layer_is_spatial" ), true ).toBool() )
498         {
499           QgsFeature f;
500           QgsFeatureRequest request;
501           request.setFilterFid( fid );
502           bool fetched = layer->getFeatures( request ).nextFeature( f );
503           if ( !fetched )
504             return;
505           QgsFeatureAction action( tr( "Attributes changed" ), f, layer, QString(), -1, QgisApp::instance() );
506           if ( layer->isEditable() )
507           {
508             action.editFeature( false );
509           }
510           else
511           {
512             action.viewFeatureForm();
513           }
514         }
515         else
516         {
517           QgisApp::instance()->mapCanvas()->zoomToFeatureIds( layer, QgsFeatureIds() << fid );
518           QgisApp::instance()->mapCanvas()->flashFeatureIds( layer, QgsFeatureIds() << fid );
519         }
520       }
521       break;
522     }
523     case ResultType::FieldRestriction:
524     {
525       // this is a field restriction
526       QgisApp::instance()->locatorWidget()->search( data.value( QStringLiteral( "search_text" ) ).toString() );
527       break;
528     }
529   }
530 }
531 
openConfigWidget(QWidget * parent)532 void QgsActiveLayerFeaturesLocatorFilter::openConfigWidget( QWidget *parent )
533 {
534   QString key = "locator_filters/active_layer_features";
535   QgsSettings settings;
536   std::unique_ptr<QDialog> dlg( new QDialog( parent ) );
537   dlg->restoreGeometry( settings.value( QStringLiteral( "Windows/%1/geometry" ).arg( key ) ).toByteArray() );
538   dlg->setWindowTitle( "All layers features locator filter" );
539   QFormLayout *formLayout = new QFormLayout;
540   QSpinBox *globalLimitSpinBox = new QSpinBox( dlg.get() );
541   globalLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_global" ).arg( key ), 30, QgsSettings::App ).toInt() );
542   globalLimitSpinBox->setMinimum( 1 );
543   globalLimitSpinBox->setMaximum( 200 );
544   formLayout->addRow( tr( "&Maximum number of results:" ), globalLimitSpinBox );
545   QDialogButtonBox *buttonbBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg.get() );
546   formLayout->addRow( buttonbBox );
547   dlg->setLayout( formLayout );
548   connect( buttonbBox, &QDialogButtonBox::accepted, [&]()
549   {
550     settings.setValue( QStringLiteral( "%1/limit_global" ).arg( key ), globalLimitSpinBox->value(), QgsSettings::App );
551     dlg->accept();
552   } );
553   connect( buttonbBox, &QDialogButtonBox::rejected, dlg.get(), &QDialog::reject );
554   dlg->exec();
555 }
556 
557 //
558 // QgsAllLayersFeaturesLocatorFilter
559 //
560 
QgsAllLayersFeaturesLocatorFilter(QObject * parent)561 QgsAllLayersFeaturesLocatorFilter::QgsAllLayersFeaturesLocatorFilter( QObject *parent )
562   : QgsLocatorFilter( parent )
563 {
564   setUseWithoutPrefix( false );
565 }
566 
clone() const567 QgsAllLayersFeaturesLocatorFilter *QgsAllLayersFeaturesLocatorFilter::clone() const
568 {
569   return new QgsAllLayersFeaturesLocatorFilter();
570 }
571 
prepare(const QString & string,const QgsLocatorContext & context)572 QStringList QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
573 {
574   // Normally skip very short search strings, unless when specifically searching using this filter
575   if ( string.length() < 3 && !context.usingPrefix )
576     return QStringList();
577 
578   QgsSettings settings;
579   mMaxTotalResults = settings.value( "locator_filters/all_layers_features/limit_global", 15, QgsSettings::App ).toInt();
580   mMaxResultsPerLayer = settings.value( "locator_filters/all_layers_features/limit_per_layer", 8, QgsSettings::App ).toInt();
581 
582   mPreparedLayers.clear();
583   const QMap<QString, QgsMapLayer *> layers = QgsProject::instance()->mapLayers();
584   for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
585   {
586     QgsVectorLayer *layer = qobject_cast< QgsVectorLayer *>( it.value() );
587     if ( !layer || !layer->dataProvider() || !layer->flags().testFlag( QgsMapLayer::Searchable ) )
588       continue;
589 
590     QgsExpression expression( layer->displayExpression() );
591     QgsExpressionContext context;
592     context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
593     expression.prepare( &context );
594 
595     QgsFeatureRequest req;
596     req.setSubsetOfAttributes( qgis::setToList( expression.referencedAttributeIndexes( layer->fields() ) ) );
597     if ( !expression.needsGeometry() )
598       req.setFlags( QgsFeatureRequest::NoGeometry );
599     QString enhancedSearch = string;
600     enhancedSearch.replace( ' ', '%' );
601     req.setFilterExpression( QStringLiteral( "%1 ILIKE '%%2%'" )
602                              .arg( layer->displayExpression(), enhancedSearch ) );
603     req.setLimit( mMaxResultsPerLayer );
604 
605     QgsFeatureRequest exactMatchRequest = req;
606     exactMatchRequest.setFilterExpression( QStringLiteral( "%1 ILIKE '%2'" )
607                                            .arg( layer->displayExpression(), enhancedSearch ) );
608     exactMatchRequest.setLimit( mMaxResultsPerLayer );
609 
610     std::shared_ptr<PreparedLayer> preparedLayer( new PreparedLayer() );
611     preparedLayer->expression = expression;
612     preparedLayer->context = context;
613     preparedLayer->layerId = layer->id();
614     preparedLayer->layerName = layer->name();
615     preparedLayer->featureSource.reset( new QgsVectorLayerFeatureSource( layer ) );
616     preparedLayer->request = req;
617     preparedLayer->exactMatchRequest = exactMatchRequest;
618     preparedLayer->layerIcon = QgsMapLayerModel::iconForLayer( layer );
619     preparedLayer->layerIsSpatial = layer->isSpatial();
620 
621     mPreparedLayers.append( preparedLayer );
622   }
623 
624   return QStringList();
625 }
626 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback * feedback)627 void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
628 {
629   int foundInCurrentLayer;
630   int foundInTotal = 0;
631   QgsFeature f;
632 
633   // we cannot used const loop since iterator::nextFeature is not const
634   for ( auto preparedLayer : qgis::as_const( mPreparedLayers ) )
635   {
636     foundInCurrentLayer = 0;
637 
638     QgsFeatureIds foundFeatureIds;
639 
640     QgsFeatureIterator exactMatchIt = preparedLayer->featureSource->getFeatures( preparedLayer->exactMatchRequest );
641     while ( exactMatchIt.nextFeature( f ) )
642     {
643       if ( feedback->isCanceled() )
644         return;
645 
646       QgsLocatorResult result;
647       result.group = preparedLayer->layerName;
648 
649       preparedLayer->context.setFeature( f );
650 
651       result.displayString = preparedLayer->expression.evaluate( &( preparedLayer->context ) ).toString();
652 
653       result.userData = QVariantList() << f.id() << preparedLayer->layerId << preparedLayer->layerIsSpatial;
654       foundFeatureIds << f.id();
655       result.icon = preparedLayer->layerIcon;
656       result.score = static_cast< double >( string.length() ) / result.displayString.size();
657 
658       if ( preparedLayer->layerIsSpatial )
659         result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
660       emit resultFetched( result );
661 
662       foundInCurrentLayer++;
663       foundInTotal++;
664       if ( foundInCurrentLayer >= mMaxResultsPerLayer )
665         break;
666     }
667     if ( foundInCurrentLayer >= mMaxResultsPerLayer )
668       continue;
669     if ( foundInTotal >= mMaxTotalResults )
670       break;
671 
672     QgsFeatureIterator it = preparedLayer->featureSource->getFeatures( preparedLayer->request );
673     while ( it.nextFeature( f ) )
674     {
675       if ( feedback->isCanceled() )
676         return;
677 
678       if ( foundFeatureIds.contains( f.id() ) )
679         continue;
680 
681       QgsLocatorResult result;
682       result.group = preparedLayer->layerName;
683 
684       preparedLayer->context.setFeature( f );
685 
686       result.displayString = preparedLayer->expression.evaluate( &( preparedLayer->context ) ).toString();
687 
688       result.userData = QVariantList() << f.id() << preparedLayer->layerId << preparedLayer->layerIsSpatial;
689       result.icon = preparedLayer->layerIcon;
690       result.score = static_cast< double >( string.length() ) / result.displayString.size();
691 
692       if ( preparedLayer->layerIsSpatial )
693         result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
694       emit resultFetched( result );
695 
696       foundInCurrentLayer++;
697       foundInTotal++;
698       if ( foundInCurrentLayer >= mMaxResultsPerLayer )
699         break;
700     }
701     if ( foundInTotal >= mMaxTotalResults )
702       break;
703   }
704 }
705 
triggerResult(const QgsLocatorResult & result)706 void QgsAllLayersFeaturesLocatorFilter::triggerResult( const QgsLocatorResult &result )
707 {
708   triggerResultFromAction( result, NoEntry );
709 }
710 
triggerResultFromAction(const QgsLocatorResult & result,const int actionId)711 void QgsAllLayersFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId )
712 {
713   QVariantList dataList = result.userData.toList();
714   QgsFeatureId fid = dataList.at( 0 ).toLongLong();
715   QString layerId = dataList.at( 1 ).toString();
716   bool layerIsSpatial = dataList.at( 2 ).toBool();
717   QgsVectorLayer *layer = QgsProject::instance()->mapLayer<QgsVectorLayer *>( layerId );
718   if ( !layer )
719     return;
720 
721   if ( actionId == OpenForm || !layerIsSpatial )
722   {
723     QgsFeature f;
724     QgsFeatureRequest request;
725     request.setFilterFid( fid );
726     bool fetched = layer->getFeatures( request ).nextFeature( f );
727     if ( !fetched )
728       return;
729     QgsFeatureAction action( tr( "Attributes changed" ), f, layer, QString(), -1, QgisApp::instance() );
730     if ( layer->isEditable() )
731     {
732       action.editFeature( false );
733     }
734     else
735     {
736       action.viewFeatureForm();
737     }
738   }
739   else
740   {
741     QgisApp::instance()->mapCanvas()->zoomToFeatureIds( layer, QgsFeatureIds() << fid );
742     QgisApp::instance()->mapCanvas()->flashFeatureIds( layer, QgsFeatureIds() << fid );
743   }
744 }
745 
openConfigWidget(QWidget * parent)746 void QgsAllLayersFeaturesLocatorFilter::openConfigWidget( QWidget *parent )
747 {
748   QString key = "locator_filters/all_layers_features";
749   QgsSettings settings;
750   std::unique_ptr<QDialog> dlg( new QDialog( parent ) );
751   dlg->restoreGeometry( settings.value( QStringLiteral( "Windows/%1/geometry" ).arg( key ) ).toByteArray() );
752   dlg->setWindowTitle( "All layers features locator filter" );
753   QFormLayout *formLayout = new QFormLayout;
754   QSpinBox *globalLimitSpinBox = new QSpinBox( dlg.get() );
755   globalLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_global" ).arg( key ), 15, QgsSettings::App ).toInt() );
756   globalLimitSpinBox->setMinimum( 1 );
757   globalLimitSpinBox->setMaximum( 200 );
758   formLayout->addRow( tr( "&Maximum number of results:" ), globalLimitSpinBox );
759   QSpinBox *perLayerLimitSpinBox = new QSpinBox( dlg.get() );
760   perLayerLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_per_layer" ).arg( key ), 8, QgsSettings::App ).toInt() );
761   perLayerLimitSpinBox->setMinimum( 1 );
762   perLayerLimitSpinBox->setMaximum( 200 );
763   formLayout->addRow( tr( "&Maximum number of results per layer:" ), perLayerLimitSpinBox );
764   QDialogButtonBox *buttonbBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg.get() );
765   formLayout->addRow( buttonbBox );
766   dlg->setLayout( formLayout );
767   connect( buttonbBox, &QDialogButtonBox::accepted, [&]()
768   {
769     settings.setValue( QStringLiteral( "%1/limit_global" ).arg( key ), globalLimitSpinBox->value(), QgsSettings::App );
770     settings.setValue( QStringLiteral( "%1/limit_per_layer" ).arg( key ), perLayerLimitSpinBox->value(), QgsSettings::App );
771     dlg->accept();
772   } );
773   connect( buttonbBox, &QDialogButtonBox::rejected, dlg.get(), &QDialog::reject );
774   dlg->exec();
775 }
776 
777 
778 //
779 // QgsExpressionCalculatorLocatorFilter
780 //
QgsExpressionCalculatorLocatorFilter(QObject * parent)781 QgsExpressionCalculatorLocatorFilter::QgsExpressionCalculatorLocatorFilter( QObject *parent )
782   : QgsLocatorFilter( parent )
783 {
784   setUseWithoutPrefix( false );
785 }
786 
clone() const787 QgsExpressionCalculatorLocatorFilter *QgsExpressionCalculatorLocatorFilter::clone() const
788 {
789   return new QgsExpressionCalculatorLocatorFilter();
790 }
791 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback *)792 void QgsExpressionCalculatorLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback * )
793 {
794   QgsExpressionContext context;
795   context << QgsExpressionContextUtils::globalScope()
796           << QgsExpressionContextUtils::projectScope( QgsProject::instance() )
797           << QgsExpressionContextUtils::layerScope( QgisApp::instance()->activeLayer() );
798 
799   QString error;
800   if ( QgsExpression::checkExpression( string, &context, error ) )
801   {
802     QgsExpression exp( string );
803     QString resultString = exp.evaluate( &context ).toString();
804     if ( !resultString.isEmpty() )
805     {
806       QgsLocatorResult result;
807       result.filter = this;
808       result.displayString = tr( "Copy “%1” to clipboard" ).arg( resultString );
809       result.userData = resultString;
810       result.score = 1;
811       emit resultFetched( result );
812     }
813   }
814 }
815 
triggerResult(const QgsLocatorResult & result)816 void QgsExpressionCalculatorLocatorFilter::triggerResult( const QgsLocatorResult &result )
817 {
818   QApplication::clipboard()->setText( result.userData.toString() );
819 }
820 
821 // SettingsLocatorFilter
822 //
QgsSettingsLocatorFilter(QObject * parent)823 QgsSettingsLocatorFilter::QgsSettingsLocatorFilter( QObject *parent )
824   : QgsLocatorFilter( parent )
825 {}
826 
clone() const827 QgsSettingsLocatorFilter *QgsSettingsLocatorFilter::clone() const
828 {
829   return new QgsSettingsLocatorFilter();
830 }
831 
fetchResults(const QString & string,const QgsLocatorContext & context,QgsFeedback *)832 void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback * )
833 {
834   QMap<QString, QMap<QString, QString>> matchingSettingsPagesMap;
835 
836   QMap<QString, int > optionsPagesMap = QgisApp::instance()->optionsPagesMap();
837   for ( auto optionsPagesIterator = optionsPagesMap.constBegin(); optionsPagesIterator != optionsPagesMap.constEnd(); ++optionsPagesIterator )
838   {
839     QString title = optionsPagesIterator.key();
840     matchingSettingsPagesMap.insert( title + " (" + tr( "Options" ) + ")", settingsPage( QStringLiteral( "optionpage" ), QString::number( optionsPagesIterator.value() ) ) );
841   }
842 
843   QMap<QString, QString> projectPropertyPagesMap = QgisApp::instance()->projectPropertiesPagesMap();
844   for ( auto projectPropertyPagesIterator = projectPropertyPagesMap.constBegin(); projectPropertyPagesIterator != projectPropertyPagesMap.constEnd(); ++projectPropertyPagesIterator )
845   {
846     QString title = projectPropertyPagesIterator.key();
847     matchingSettingsPagesMap.insert( title + " (" + tr( "Project Properties" ) + ")", settingsPage( QStringLiteral( "projectpropertypage" ), projectPropertyPagesIterator.value() ) );
848   }
849 
850   QMap<QString, QString> settingPagesMap = QgisApp::instance()->settingPagesMap();
851   for ( auto settingPagesIterator = settingPagesMap.constBegin(); settingPagesIterator != settingPagesMap.constEnd(); ++settingPagesIterator )
852   {
853     QString title = settingPagesIterator.key();
854     matchingSettingsPagesMap.insert( title, settingsPage( QStringLiteral( "settingspage" ), settingPagesIterator.value() ) );
855   }
856 
857   for ( auto matchingSettingsPagesIterator = matchingSettingsPagesMap.constBegin(); matchingSettingsPagesIterator != matchingSettingsPagesMap.constEnd(); ++matchingSettingsPagesIterator )
858   {
859     QString title = matchingSettingsPagesIterator.key();
860     QMap<QString, QString> settingsPage = matchingSettingsPagesIterator.value();
861     QgsLocatorResult result;
862     result.filter = this;
863     result.displayString = title;
864     result.userData.setValue( settingsPage );
865 
866     if ( context.usingPrefix && string.isEmpty() )
867     {
868       emit resultFetched( result );
869       continue;
870     }
871 
872     result.score = fuzzyScore( result.displayString, string );
873 
874     if ( result.score > 0 )
875       emit resultFetched( result );
876   }
877 }
878 
settingsPage(const QString & type,const QString & page)879 QMap<QString, QString> QgsSettingsLocatorFilter::settingsPage( const QString &type,  const QString &page )
880 {
881   QMap<QString, QString> returnPage;
882   returnPage.insert( QStringLiteral( "type" ), type );
883   returnPage.insert( QStringLiteral( "page" ), page );
884   return returnPage;
885 }
886 
triggerResult(const QgsLocatorResult & result)887 void QgsSettingsLocatorFilter::triggerResult( const QgsLocatorResult &result )
888 {
889 
890   QMap<QString, QString> settingsPage = qvariant_cast<QMap<QString, QString>>( result.userData );
891   QString type = settingsPage.value( QStringLiteral( "type" ) );
892   QString page = settingsPage.value( QStringLiteral( "page" ) );
893 
894   if ( type == QLatin1String( "optionpage" ) )
895   {
896     const int pageNumber = page.toInt();
897     QgisApp::instance()->showOptionsDialog( QgisApp::instance(), QString(), pageNumber );
898   }
899   else if ( type == QLatin1String( "projectpropertypage" ) )
900   {
901     QgisApp::instance()->showProjectProperties( page );
902   }
903   else if ( type == QLatin1String( "settingspage" ) )
904   {
905     QgisApp::instance()->showSettings( page );
906   }
907 }
908 
909 // QgBookmarkLocatorFilter
910 //
911 
QgsBookmarkLocatorFilter(QObject * parent)912 QgsBookmarkLocatorFilter::QgsBookmarkLocatorFilter( QObject *parent )
913   : QgsLocatorFilter( parent )
914 {}
915 
clone() const916 QgsBookmarkLocatorFilter *QgsBookmarkLocatorFilter::clone() const
917 {
918   return new QgsBookmarkLocatorFilter();
919 }
920 
fetchResults(const QString & string,const QgsLocatorContext & context,QgsFeedback * feedback)921 void QgsBookmarkLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback )
922 {
923   QMap<QString, QModelIndex> bookmarkMap = QgisApp::instance()->getBookmarkIndexMap();
924 
925   QMapIterator<QString, QModelIndex> i( bookmarkMap );
926 
927   while ( i.hasNext() )
928   {
929     i.next();
930 
931     if ( feedback->isCanceled() )
932       return;
933 
934     QString name = i.key();
935     QModelIndex index = i.value();
936     QgsLocatorResult result;
937     result.filter = this;
938     result.displayString = name;
939     result.userData = index;
940     result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/mItemBookmark.svg" ) );
941 
942     if ( context.usingPrefix && string.isEmpty() )
943     {
944       emit resultFetched( result );
945       continue;
946     }
947 
948     result.score = fuzzyScore( result.displayString, string );
949 
950     if ( result.score > 0 )
951       emit resultFetched( result );
952   }
953 }
954 
triggerResult(const QgsLocatorResult & result)955 void QgsBookmarkLocatorFilter::triggerResult( const QgsLocatorResult &result )
956 {
957   QModelIndex index = qvariant_cast<QModelIndex>( result.userData );
958   QgisApp::instance()->zoomToBookmarkIndex( index );
959 }
960 
961 //
962 // QgsGotoLocatorFilter
963 //
964 
QgsGotoLocatorFilter(QObject * parent)965 QgsGotoLocatorFilter::QgsGotoLocatorFilter( QObject *parent )
966   : QgsLocatorFilter( parent )
967 {}
968 
clone() const969 QgsGotoLocatorFilter *QgsGotoLocatorFilter::clone() const
970 {
971   return new QgsGotoLocatorFilter();
972 }
973 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback * feedback)974 void QgsGotoLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
975 {
976   if ( feedback->isCanceled() )
977     return;
978 
979   const QgsCoordinateReferenceSystem currentCrs = QgisApp::instance()->mapCanvas()->mapSettings().destinationCrs();
980   const QgsCoordinateReferenceSystem wgs84Crs( QStringLiteral( "EPSG:4326" ) );
981 
982   bool okX = false;
983   bool okY = false;
984   double posX = 0.0;
985   double posY = 0.0;
986   bool posIsDms = false;
987   QLocale locale;
988 
989   // Coordinates such as 106.8468,-6.3804
990   QRegularExpression separatorRx( QStringLiteral( "^([0-9\\-\\%1\\%2]*)[\\s%3]*([0-9\\-\\%1\\%2]*)$" ).arg( locale.decimalPoint(),
991                                   locale.groupSeparator(),
992                                   locale.decimalPoint() != ',' && locale.groupSeparator() != ',' ? QStringLiteral( "\\," ) : QString() ) );
993   QRegularExpressionMatch match = separatorRx.match( string.trimmed() );
994   if ( match.hasMatch() )
995   {
996     posX = locale.toDouble( match.captured( 1 ), &okX );
997     posY = locale.toDouble( match.captured( 2 ), &okY );
998   }
999 
1000   if ( !match.hasMatch() || !okX || !okY )
1001   {
1002     // Digit detection using user locale failed, use default C decimal separators
1003     separatorRx = QRegularExpression( QStringLiteral( "^([0-9\\-\\.]*)[\\s\\,]*([0-9\\-\\.]*)$" ) );
1004     match = separatorRx.match( string.trimmed() );
1005     if ( match.hasMatch() )
1006     {
1007       posX = match.captured( 1 ).toDouble( &okX );
1008       posY = match.captured( 2 ).toDouble( &okY );
1009     }
1010   }
1011 
1012   if ( !match.hasMatch() )
1013   {
1014     // Check if the string is a pair of degree minute second
1015     separatorRx = QRegularExpression( QStringLiteral( "^((?:([-+nsew])\\s*)?\\d{1,3}(?:[^0-9.]+[0-5]?\\d)?[^0-9.]+[0-5]?\\d(?:\\.\\d+)?[^0-9.,]*[-+nsew]?)[,\\s]+((?:([-+nsew])\\s*)?\\d{1,3}(?:[^0-9.]+[0-5]?\\d)?[^0-9.]+[0-5]?\\d(?:\\.\\d+)?[^0-9.,]*[-+nsew]?)$" ) );
1016     match = separatorRx.match( string.trimmed() );
1017     if ( match.hasMatch() )
1018     {
1019       posIsDms = true;
1020       bool isEasting = false;
1021       posX = QgsCoordinateUtils::dmsToDecimal( match.captured( 1 ), &okX, &isEasting );
1022       posY = QgsCoordinateUtils::dmsToDecimal( match.captured( 3 ), &okY );
1023       if ( !isEasting )
1024         std::swap( posX, posY );
1025     }
1026   }
1027 
1028   if ( okX && okY )
1029   {
1030     QVariantMap data;
1031     QgsPointXY point( posX, posY );
1032     data.insert( QStringLiteral( "point" ), point );
1033 
1034     bool withinWgs84 = wgs84Crs.bounds().contains( point );
1035     if ( !posIsDms && currentCrs != wgs84Crs )
1036     {
1037       QgsLocatorResult result;
1038       result.filter = this;
1039       result.displayString = tr( "Go to %1 %2 (Map CRS, %3)" ).arg( locale.toString( point.x(), 'g', 10 ), locale.toString( point.y(), 'g', 10 ), currentCrs.userFriendlyIdentifier() );
1040       result.userData = data;
1041       result.score = 0.9;
1042       emit resultFetched( result );
1043     }
1044 
1045     if ( withinWgs84 )
1046     {
1047       if ( currentCrs != wgs84Crs )
1048       {
1049         QgsCoordinateTransform transform( wgs84Crs, currentCrs, QgsProject::instance()->transformContext() );
1050         QgsPointXY transformedPoint;
1051         try
1052         {
1053           transformedPoint = transform.transform( point );
1054         }
1055         catch ( const QgsException &e )
1056         {
1057           Q_UNUSED( e )
1058           return;
1059         }
1060         data[QStringLiteral( "point" )] = transformedPoint;
1061       }
1062 
1063       QgsLocatorResult result;
1064       result.filter = this;
1065       result.displayString = tr( "Go to %1° %2° (%3)" ).arg( locale.toString( point.x(), 'g', 10 ), locale.toString( point.y(), 'g', 10 ), wgs84Crs.userFriendlyIdentifier() );
1066       result.userData = data;
1067       result.score = 1.0;
1068       emit resultFetched( result );
1069     }
1070     return;
1071   }
1072 
1073   QMap<int, double> scales;
1074   scales[0] = 739571909;
1075   scales[1] = 369785954;
1076   scales[2] = 184892977;
1077   scales[3] = 92446488;
1078   scales[4] = 46223244;
1079   scales[5] = 23111622;
1080   scales[6] = 11555811;
1081   scales[7] = 5777905;
1082   scales[8] = 2888952;
1083   scales[9] = 1444476;
1084   scales[10] = 722238;
1085   scales[11] = 361119;
1086   scales[12] = 180559;
1087   scales[13] = 90279;
1088   scales[14] = 45139;
1089   scales[15] = 22569;
1090   scales[16] = 11284;
1091   scales[17] = 5642;
1092   scales[18] = 2821;
1093   scales[19] = 1500;
1094   scales[20] = 1000;
1095   scales[21] = 282;
1096 
1097   QUrl url( string );
1098   if ( url.isValid() )
1099   {
1100     double scale = 0.0;
1101     int meters = 0;
1102     okX = false;
1103     okY = false;
1104     posX = 0.0;
1105     posY = 0.0;
1106     if ( url.hasFragment() )
1107     {
1108       // Check for OSM/Leaflet/OpenLayers pattern (e.g. http://www.openstreetmap.org/#map=6/46.423/4.746)
1109       QStringList fragments = url.fragment().split( '&' );
1110       for ( const QString &fragment : fragments )
1111       {
1112         if ( fragment.startsWith( QLatin1String( "map=" ) ) )
1113         {
1114           QStringList params = fragment.mid( 4 ).split( '/' );
1115           if ( params.size() >= 3 )
1116           {
1117             if ( scales.contains( params.at( 0 ).toInt() ) )
1118             {
1119               scale = scales.value( params.at( 0 ).toInt() );
1120             }
1121             posX = params.at( 2 ).toDouble( &okX );
1122             posY = params.at( 1 ).toDouble( &okY );
1123           }
1124           break;
1125         }
1126       }
1127     }
1128 
1129     if ( !okX && !okY )
1130     {
1131       QRegularExpression locationRx( QStringLiteral( "google.*\\/@([0-9\\-\\.\\,]*)(z|m|a)" ) );
1132       match = locationRx.match( string );
1133       if ( match.hasMatch() )
1134       {
1135         QStringList params = match.captured( 1 ).split( ',' );
1136         if ( params.size() == 3 )
1137         {
1138           posX = params.at( 1 ).toDouble( &okX );
1139           posY = params.at( 0 ).toDouble( &okY );
1140 
1141           if ( okX && okY )
1142           {
1143             if ( match.captured( 2 ) == QChar( 'z' ) && scales.contains( static_cast<int>( params.at( 2 ).toDouble() ) ) )
1144             {
1145               scale = scales.value( static_cast<int>( params.at( 2 ).toDouble() ) );
1146             }
1147             else if ( match.captured( 2 ) == QChar( 'm' ) )
1148             {
1149               // satellite view URL, scale to be derived from canvas height
1150               meters = params.at( 2 ).toInt();
1151             }
1152             else if ( match.captured( 2 ) == QChar( 'a' ) )
1153             {
1154               // street view URL, use most zoomed in scale value
1155               scale = scales.value( 21 );
1156             }
1157           }
1158         }
1159       }
1160     }
1161 
1162     if ( okX && okY )
1163     {
1164       QVariantMap data;
1165       QgsPointXY point( posX, posY );
1166       QgsPointXY dataPoint = point;
1167       bool withinWgs84 = wgs84Crs.bounds().contains( point );
1168       if ( withinWgs84 && currentCrs != wgs84Crs )
1169       {
1170         QgsCoordinateTransform transform( wgs84Crs, currentCrs, QgsProject::instance()->transformContext() );
1171         dataPoint = transform.transform( point );
1172       }
1173       data.insert( QStringLiteral( "point" ), dataPoint );
1174 
1175       if ( meters > 0 )
1176       {
1177         QSize outputSize = QgisApp::instance()->mapCanvas()->mapSettings().outputSize();
1178         QgsDistanceArea da;
1179         da.setSourceCrs( currentCrs, QgsProject::instance()->transformContext() );
1180         da.setEllipsoid( QgsProject::instance()->ellipsoid() );
1181         double height = da.measureLineProjected( dataPoint, meters );
1182         double width = outputSize.width() * ( height / outputSize.height() );
1183 
1184         QgsRectangle extent;
1185         extent.setYMinimum( dataPoint.y() -  height / 2.0 );
1186         extent.setYMaximum( dataPoint.y() +  height / 2.0 );
1187         extent.setXMinimum( dataPoint.x() -  width / 2.0 );
1188         extent.setXMaximum( dataPoint.x() +  width / 2.0 );
1189 
1190         QgsScaleCalculator calculator;
1191         calculator.setMapUnits( currentCrs.mapUnits() );
1192         calculator.setDpi( QgisApp::instance()->mapCanvas()->mapSettings().outputDpi() );
1193         scale = calculator.calculate( extent, outputSize.width() );
1194       }
1195 
1196       if ( scale > 0.0 )
1197       {
1198         data.insert( QStringLiteral( "scale" ), scale );
1199       }
1200 
1201       QgsLocatorResult result;
1202       result.filter = this;
1203       result.displayString = tr( "Go to %1° %2° %3(%4)" ).arg( locale.toString( point.x(), 'g', 10 ), locale.toString( point.y(), 'g', 10 ),
1204                              scale > 0.0 ? tr( "at scale 1:%1 " ).arg( scale ) : QString(),
1205                              wgs84Crs.userFriendlyIdentifier() );
1206       result.userData = data;
1207       result.score = 1.0;
1208       emit resultFetched( result );
1209     }
1210   }
1211 }
1212 
triggerResult(const QgsLocatorResult & result)1213 void QgsGotoLocatorFilter::triggerResult( const QgsLocatorResult &result )
1214 {
1215   QgsMapCanvas *mapCanvas = QgisApp::instance()->mapCanvas();
1216 
1217   QVariantMap data = result.userData.toMap();
1218   QgsPointXY point = data[QStringLiteral( "point" )].value<QgsPointXY>();
1219   mapCanvas->setCenter( point );
1220   if ( data.contains( QStringLiteral( "scale" ) ) )
1221   {
1222     mapCanvas->zoomScale( data[QStringLiteral( "scale" )].toDouble() );
1223   }
1224   else
1225   {
1226     mapCanvas->refresh();
1227   }
1228 
1229   mapCanvas->flashGeometries( QList< QgsGeometry >() << QgsGeometry::fromPointXY( point ) );
1230 }
1231