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