1 /***************************************************************************
2      testqgsapplocatorfilters.cpp
3      --------------------------
4     Date                 : 2018-02-24
5     Copyright            : (C) 2018 by Nyall Dawson
6     Email                : nyall dot dawson at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 #include "qgstest.h"
16 #include "qgisapp.h"
17 #include "qgslocatorfilter.h"
18 #include "qgslocator.h"
19 #include "qgsprintlayout.h"
20 #include "qgslayoutmanager.h"
21 #include "locator/qgsinbuiltlocatorfilters.h"
22 #include <QSignalSpy>
23 #include <QClipboard>
24 
25 /**
26  * \ingroup UnitTests
27  * This is a unit test for the field calculator
28  */
29 class TestQgsAppLocatorFilters : public QObject
30 {
31     Q_OBJECT
32 
33   private slots:
34     void initTestCase();// will be called before the first testfunction is executed.
35     void cleanupTestCase();// will be called after the last testfunction was executed.
36     void testCalculator();
37     void testLayers();
38     void testLayouts();
39     void testSearchActiveLayer();
40     void testActiveLayerFieldRestriction();
41     void testActiveLayerCompletion();
42     void testSearchAllLayers();
43     void testSearchAllLayersPrioritizeExactMatch();
44     void testGoto();
45 
46   private:
47     QgisApp *mQgisApp = nullptr;
48 
49     QList< QgsLocatorResult > gatherResults( QgsLocatorFilter *filter, const QString &string, const QgsLocatorContext &context );
50 };
51 
52 //runs before all tests
initTestCase()53 void TestQgsAppLocatorFilters::initTestCase()
54 {
55   // init QGIS's paths - true means that all path will be inited from prefix
56   QgsApplication::init();
57   QgsApplication::initQgis();
58   mQgisApp = new QgisApp();
59 }
60 
61 //runs after all tests
cleanupTestCase()62 void TestQgsAppLocatorFilters::cleanupTestCase()
63 {
64   QgsApplication::exitQgis();
65 }
66 
testCalculator()67 void TestQgsAppLocatorFilters::testCalculator()
68 {
69   QgsExpressionCalculatorLocatorFilter filter;
70 
71   // valid expression
72   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "1+2" ), QgsLocatorContext() );
73   QCOMPARE( results.count(), 1 );
74   QCOMPARE( results.at( 0 ).userData.toInt(), 3 );
75 
76   // trigger result
77   filter.triggerResult( results.at( 0 ) );
78   QCOMPARE( QApplication::clipboard()->text(), QStringLiteral( "3" ) );
79 
80   // invalid expression
81   results = gatherResults( &filter, QStringLiteral( "1+" ), QgsLocatorContext() );
82   QVERIFY( results.empty() );
83 }
84 
testLayers()85 void TestQgsAppLocatorFilters::testLayers()
86 {
87   QgsVectorLayer *l1 = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "aaaaa" ), QStringLiteral( "memory" ) );
88   QgsVectorLayer *l2 = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "abc" ), QStringLiteral( "memory" ) );
89   QgsVectorLayer *l3 = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "ccccc" ), QStringLiteral( "memory" ) );
90   QgsProject::instance()->addMapLayers( QList< QgsMapLayer *>() << l1 << l2 << l3 );
91 
92   QgsLayerTreeLocatorFilter filter;
93 
94   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "xxxxx" ), QgsLocatorContext() );
95   QCOMPARE( results.count(), 0 );
96 
97   results = gatherResults( &filter, QStringLiteral( "aa" ), QgsLocatorContext() );
98   QCOMPARE( results.count(), 1 );
99   QCOMPARE( results.at( 0 ).userData.toString(), l1->id() );
100 
101   results = gatherResults( &filter, QStringLiteral( "A" ), QgsLocatorContext() );
102   QCOMPARE( results.count(), 2 );
103   QCOMPARE( results.at( 0 ).userData.toString(), l1->id() );
104   QCOMPARE( results.at( 1 ).userData.toString(), l2->id() );
105 
106   results = gatherResults( &filter, QString(), QgsLocatorContext() );
107   QCOMPARE( results.count(), 0 );
108 
109   QgsLocatorContext context;
110   context.usingPrefix = true;
111   results = gatherResults( &filter, QString(), context );
112   QCOMPARE( results.count(), 3 );
113   QCOMPARE( results.at( 0 ).userData.toString(), l1->id() );
114   QCOMPARE( results.at( 1 ).userData.toString(), l2->id() );
115   QCOMPARE( results.at( 2 ).userData.toString(), l3->id() );
116 }
117 
testLayouts()118 void TestQgsAppLocatorFilters::testLayouts()
119 {
120   QgsPrintLayout *pl1 = new QgsPrintLayout( QgsProject::instance() );
121   pl1->setName( QStringLiteral( "aaaaaa" ) );
122   QgsProject::instance()->layoutManager()->addLayout( pl1 );
123   QgsPrintLayout *pl2 = new QgsPrintLayout( QgsProject::instance() );
124   pl2->setName( QStringLiteral( "abc" ) );
125   QgsProject::instance()->layoutManager()->addLayout( pl2 );
126   QgsPrintLayout *pl3 = new QgsPrintLayout( QgsProject::instance() );
127   pl3->setName( QStringLiteral( "ccccc" ) );
128   QgsProject::instance()->layoutManager()->addLayout( pl3 );
129 
130   QgsLayoutLocatorFilter filter;
131 
132   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "xxxxx" ), QgsLocatorContext() );
133   QCOMPARE( results.count(), 0 );
134 
135   results = gatherResults( &filter, QStringLiteral( "aa" ), QgsLocatorContext() );
136   QCOMPARE( results.count(), 1 );
137   QCOMPARE( results.at( 0 ).userData.toString(), pl1->name() );
138 
139   results = gatherResults( &filter, QStringLiteral( "A" ), QgsLocatorContext() );
140   QCOMPARE( results.count(), 2 );
141   QCOMPARE( results.at( 0 ).userData.toString(), pl1->name() );
142   QCOMPARE( results.at( 1 ).userData.toString(), pl2->name() );
143 
144   results = gatherResults( &filter, QString(), QgsLocatorContext() );
145   QCOMPARE( results.count(), 0 );
146 
147   QgsLocatorContext context;
148   context.usingPrefix = true;
149   results = gatherResults( &filter, QString(), context );
150   QCOMPARE( results.count(), 3 );
151   QCOMPARE( results.at( 0 ).userData.toString(), pl1->name() );
152   QCOMPARE( results.at( 1 ).userData.toString(), pl2->name() );
153   QCOMPARE( results.at( 2 ).userData.toString(), pl3->name() );
154 }
155 
testSearchActiveLayer()156 void TestQgsAppLocatorFilters::testSearchActiveLayer()
157 {
158   QString layerDef = QStringLiteral( "Point?crs=epsg:4326&field=pk:integer&field=my_text:string&field=my_integer:integer&field=my_double:double&key=pk" );
159   QgsVectorLayer *vl = new QgsVectorLayer( layerDef, QStringLiteral( "Layer" ), QStringLiteral( "memory" ) );
160   QgsProject::instance()->addMapLayer( vl );
161 
162   QgsFeature f;
163   f.setAttributes( QVector<QVariant>() << 1001 << "A nice feature" << 1234567890 << 12345.6789 );
164   f.setGeometry( QgsGeometry::fromWkt( "Point (-71.123 78.23)" ) );
165   vl->dataProvider()->addFeature( f );
166   QgsFeature f2;
167   f2.setAttributes( QVector<QVariant>() << 100 << "@home" << 13579 << 13.57 );
168   f2.setGeometry( QgsGeometry::fromWkt( "Point (-71.223 78.33)" ) );
169   vl->dataProvider()->addFeature( f2 );
170 
171   mQgisApp->setActiveLayer( vl );
172 
173   QgsActiveLayerFeaturesLocatorFilter filter;
174   QgsLocatorContext context;
175 
176   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "12345.6789" ), context );
177   QCOMPARE( results.count(), 1 );
178 
179   results = gatherResults( &filter, QStringLiteral( "12345.67" ), context );
180   QCOMPARE( results.count(), 0 );
181 
182   results = gatherResults( &filter, QStringLiteral( "1234567890" ), context );
183   QCOMPARE( results.count(), 1 );
184 
185   results = gatherResults( &filter, QStringLiteral( "nice" ), context );
186   QCOMPARE( results.count(), 1 );
187 
188   results = gatherResults( &filter, QStringLiteral( "@my_text nice" ), context );
189   QCOMPARE( results.count(), 1 );
190 
191   results = gatherResults( &filter, QStringLiteral( "@my_integer nice" ), context );
192   QCOMPARE( results.count(), 0 );
193 
194   results = gatherResults( &filter, QStringLiteral( "@unknown_field nice" ), context );
195   QCOMPARE( results.count(), 0 );
196 
197   // check with display expression, feature should not be shown twice
198   vl->setDisplayExpression( QStringLiteral( "concat(\"my_text\", ' ', \"my_double\")" ) );
199   results = gatherResults( &filter, QStringLiteral( "nice" ), context );
200   QCOMPARE( results.count(), 1 );
201   results = gatherResults( &filter, QStringLiteral( "a feature" ), context );
202   QCOMPARE( results.count(), 1 );
203   results = gatherResults( &filter, QStringLiteral( "nice .678" ), context );
204   QCOMPARE( results.count(), 1 );
205 
206   results = gatherResults( &filter, QStringLiteral( "@my_text @home" ), context );
207   QCOMPARE( results.count(), 1 );
208 
209   QgsProject::instance()->removeAllMapLayers();
210 }
211 
testActiveLayerFieldRestriction()212 void TestQgsAppLocatorFilters::testActiveLayerFieldRestriction()
213 {
214   bool isRestricting = false;
215 
216   QString search = QStringLiteral( "@my_field search" );
217   QString restr = QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( search, &isRestricting );
218   QVERIFY( isRestricting );
219   QCOMPARE( restr, QStringLiteral( "my_field" ) );
220   QCOMPARE( search, QStringLiteral( "search" ) );
221 
222   search = QStringLiteral( "@home" );
223   restr = QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( search, &isRestricting );
224   QVERIFY( isRestricting );
225   QCOMPARE( restr, QStringLiteral( "home" ) );
226   QCOMPARE( search, QStringLiteral( "" ) );
227 
228   search = QStringLiteral( "@" );
229   restr = QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( search, &isRestricting );
230   QVERIFY( isRestricting );
231   QCOMPARE( restr, QString() );
232   QCOMPARE( search, QString() );
233 
234   search = QStringLiteral( "hello there" );
235   restr = QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( search, &isRestricting );
236   QVERIFY( !isRestricting );
237   QVERIFY( restr.isNull() );
238   QCOMPARE( search, QStringLiteral( "hello there" ) );
239 }
240 
testActiveLayerCompletion()241 void TestQgsAppLocatorFilters::testActiveLayerCompletion()
242 {
243   QString layerDef = QStringLiteral( "Point?crs=epsg:4326&field=pk:integer&field=my_text:string&field=my_integer:integer&field=my_double:double&key=pk" );
244   QgsVectorLayer *vl = new QgsVectorLayer( layerDef, QStringLiteral( "Layer" ), QStringLiteral( "memory" ) );
245   QgsProject::instance()->addMapLayer( vl );
246   mQgisApp->setActiveLayer( vl );
247 
248   QgsFeedback f;
249   QgsActiveLayerFeaturesLocatorFilter filter;
250   QgsLocatorContext context;
251   context.usingPrefix = true;
252 
253   QCOMPARE( filter.prepare( QStringLiteral( "" ), context ), QStringList( { "@pk ", "@my_text ", "@my_integer ", "@my_double " } ) );
254   QCOMPARE( filter.prepare( QStringLiteral( "@my_i" ), context ), QStringList( { "@my_integer " } ) );
255 
256   QgsProject::instance()->removeAllMapLayers();
257 }
258 
testSearchAllLayers()259 void TestQgsAppLocatorFilters::testSearchAllLayers()
260 {
261   QString layerDef = QStringLiteral( "Point?crs=epsg:4326&field=pk:integer&field=my_text:string&field=my_number:integer&key=pk" );
262   QgsVectorLayer *l1 = new QgsVectorLayer( layerDef, QStringLiteral( "Layer 1" ), QStringLiteral( "memory" ) );
263   QgsVectorLayer *l2 = new QgsVectorLayer( layerDef, QStringLiteral( "Layer 2" ), QStringLiteral( "memory" ) );
264 
265   QgsProject::instance()->addMapLayers( QList< QgsMapLayer *>() << l1 << l2 );
266 
267   QgsFeature f1;
268   f1.setAttributes( QVector<QVariant>() << 1001 << "A nice feature" << 6789 );
269   f1.setGeometry( QgsGeometry::fromWkt( "Point (-71.123 78.23)" ) );
270   QgsFeature f2;
271   f2.setAttributes( QVector<QVariant>() << 1002 << "Something crazy" << 2 );
272   f2.setGeometry( QgsGeometry::fromWkt( "Point (-72.123 78.23)" ) );
273   QgsFeature f3;
274   f3.setAttributes( QVector<QVariant>() << 2001 << "Another feature" << 6789 );
275   f3.setGeometry( QgsGeometry::fromWkt( "Point (-73.123 78.23)" ) );
276 
277   l1->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 );
278   l2->dataProvider()->addFeatures( QgsFeatureList() << f3 );
279 
280   QgsAllLayersFeaturesLocatorFilter filter;
281   QgsLocatorContext context;
282 
283   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "100" ), context );
284 
285   l1->setDisplayExpression( QStringLiteral( "\"my_text\" || ' is ' || \"my_number\"" ) );
286   l2->setDisplayExpression( QStringLiteral( "\"my_text\" || ' is ' || \"my_number\"" ) );
287 
288   results = gatherResults( &filter, QStringLiteral( "feature is 6789" ), context );
289   QCOMPARE( results.count(), 2 );
290 
291   l2->setFlags( l2->flags() & ~QgsMapLayer::Searchable );
292 
293   results = gatherResults( &filter, QStringLiteral( "feature is 6789" ), context );
294   QCOMPARE( results.count(), 1 );
295 
296   QgsProject::instance()->removeAllMapLayers();
297 }
298 
testSearchAllLayersPrioritizeExactMatch()299 void TestQgsAppLocatorFilters::testSearchAllLayersPrioritizeExactMatch()
300 {
301   QString layerDef = QStringLiteral( "Point?crs=epsg:4326&field=pk:integer&field=my_text:string&field=my_number:integer&key=pk" );
302   QgsVectorLayer *l1 = new QgsVectorLayer( layerDef, QStringLiteral( "Layer 1" ), QStringLiteral( "memory" ) );
303 
304   QgsProject::instance()->addMapLayers( QList< QgsMapLayer *>() << l1 );
305 
306   QgsFeature f1;
307   f1.setAttributes( QVector<QVariant>() << 100 << "A nice feature" << 100 );
308   f1.setGeometry( QgsGeometry::fromWkt( "Point (-71.123 78.23)" ) );
309   QgsFeature f2;
310   f2.setAttributes( QVector<QVariant>() << 101 << "Something crazy" << 3 );
311   f2.setGeometry( QgsGeometry::fromWkt( "Point (-72.123 78.23)" ) );
312   QgsFeature f3;
313   f3.setAttributes( QVector<QVariant>() << 102 << "Another feature" << 1 );
314   f3.setGeometry( QgsGeometry::fromWkt( "Point (-73.123 78.23)" ) );
315 
316   l1->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 );
317 
318   QgsAllLayersFeaturesLocatorFilter filter;
319   QgsLocatorContext context;
320   context.usingPrefix = true; // Searching for short strings is only available with prefix
321 
322   l1->setDisplayExpression( QStringLiteral( "\"my_number\"" ) );
323 
324   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "1" ), context );
325   QCOMPARE( results.count(), 2 );
326   QCOMPARE( results.first().displayString, QStringLiteral( "1" ) );
327   QCOMPARE( results.last().displayString, QStringLiteral( "100" ) );
328 
329   QgsProject::instance()->removeAllMapLayers();
330 }
331 
gatherResults(QgsLocatorFilter * filter,const QString & string,const QgsLocatorContext & context)332 QList<QgsLocatorResult> TestQgsAppLocatorFilters::gatherResults( QgsLocatorFilter *filter, const QString &string, const QgsLocatorContext &context )
333 {
334   QSignalSpy spy( filter, &QgsLocatorFilter::resultFetched );
335   QgsFeedback f;
336   filter->prepare( string, context );
337   filter->fetchResults( string, context, &f );
338 
339   QList< QgsLocatorResult > results;
340   for ( int i = 0; i < spy.count(); ++ i )
341   {
342     QVariant v = spy.at( i ).at( 0 );
343     QgsLocatorResult result = v.value<QgsLocatorResult>();
344     results.append( result );
345   }
346   return results;
347 }
348 
testGoto()349 void TestQgsAppLocatorFilters::testGoto()
350 {
351   QgsGotoLocatorFilter filter;
352 
353   // simple goto
354   QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "4 5" ), QgsLocatorContext() );
355   QCOMPARE( results.count(), 2 );
356   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 4 5 (Map CRS, )" ) );
357   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 4, 5 ) );
358   QCOMPARE( results.at( 1 ).displayString, QObject::tr( "Go to 4° 5° (EPSG:4326 - WGS 84)" ) );
359   QCOMPARE( results.at( 1 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 4, 5 ) );
360 
361   // locale-specific goto
362   results = gatherResults( &filter, QStringLiteral( "1,234.56 789.012" ), QgsLocatorContext() );
363   QCOMPARE( results.count(), 1 );
364   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 1,234.56 789.012 (Map CRS, )" ) );
365   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 1234.56, 789.012 ) );
366 
367   // degree/minuste/second coordinates goto
368   // easting northing
369   results = gatherResults( &filter, QStringLiteral( "40deg 1' 0\" E 11deg  55' 0\" S" ), QgsLocatorContext() );
370   QCOMPARE( results.count(), 1 );
371   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 40.01666667° -11.91666667° (EPSG:4326 - WGS 84)" ) );
372   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 40.0166666667, -11.9166666667 ) );
373 
374   // northing easting
375   results = gatherResults( &filter, QStringLiteral( "14°49′48″N 01°48′45″E" ), QgsLocatorContext() );
376   QCOMPARE( results.count(), 1 );
377   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 1.8125° 14.83° (EPSG:4326 - WGS 84)" ) );
378   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 1.8125, 14.83 ) );
379 
380   // northing, esting (comma separated)
381   results = gatherResults( &filter, QStringLiteral( "14°49′48″N, 01°48′45″E" ), QgsLocatorContext() );
382   QCOMPARE( results.count(), 1 );
383   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 1.8125° 14.83° (EPSG:4326 - WGS 84)" ) );
384   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 1.8125, 14.83 ) );
385 
386   // OSM/Leaflet/OpenLayers
387   results = gatherResults( &filter, QStringLiteral( "https://www.openstreetmap.org/#map=15/44.5546/6.4936" ), QgsLocatorContext() );
388   QCOMPARE( results.count(), 1 );
389   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 6.4936° 44.5546° at scale 1:22569 (EPSG:4326 - WGS 84)" ) );
390   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 6.4936, 44.5546 ) );
391   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 );
392 
393   // Google Maps
394   results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@44.5546,6.4936,15.25z" ), QgsLocatorContext() );
395   QCOMPARE( results.count(), 1 );
396   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 6.4936° 44.5546° at scale 1:22569 (EPSG:4326 - WGS 84)" ) );
397   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 6.4936, 44.5546 ) );
398   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 );
399 
400   results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@7.8750,81.0149,574195m/data=!3m1!1e3" ), QgsLocatorContext() );
401   QCOMPARE( results.count(), 1 );
402   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 81.0149, 7.8750 ) );
403 
404   results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@27.7132,85.3288,3a,75y,278.89h,90t/data=!3m8!1e1!3m6!1sAF1QipMrXuXozGc9x9bxx5uPl_3ys4H-rNVqMLr6EYLA!2e10!3e11!6shttps:%2F%2Flh5.googleusercontent.com%2Fp%2FAF1QipMrXuXozGc9x9bxx5uPl_3ys4H-rNVqMLr6EYLA%3Dw203-h100-k-no-pi2.869903-ya293.58762-ro-1.9255565-fo100!7i3840!8i1920" ), QgsLocatorContext() );
405   QCOMPARE( results.count(), 1 );
406   QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 85.3288° 27.7132° at scale 1:282 (EPSG:4326 - WGS 84)" ) );
407   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "point" )].value<QgsPointXY>(), QgsPointXY( 85.3288, 27.7132 ) );
408   QCOMPARE( results.at( 0 ).userData.toMap()[QStringLiteral( "scale" )].toDouble(), 282.0 );
409 }
410 
411 QGSTEST_MAIN( TestQgsAppLocatorFilters )
412 #include "testqgsapplocatorfilters.moc"
413