1 /***************************************************************************
2      testqgsmaptoollabel.cpp
3      -----------------------
4     Date                 : July 2019
5     Copyright            : (C) 2019 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 
16 #include "qgstest.h"
17 #include <QObject>
18 #include <QString>
19 #include <QMouseEvent>
20 
21 #include "qgsapplication.h"
22 #include "qgsvectorlayer.h"
23 #include "qgsvectordataprovider.h"
24 #include "qgsgeometry.h"
25 #include "qgsmapcanvas.h"
26 #include "qgsmaptoollabel.h"
27 #include "qgsfontutils.h"
28 #include "qgsvectorlayerlabelprovider.h"
29 #include "qgsvectorlayerlabeling.h"
30 
31 class TestQgsMapToolLabel : public QObject
32 {
33     Q_OBJECT
34 
35   public:
36     TestQgsMapToolLabel() = default;
37 
38   private:
39 
40   private slots:
41 
initTestCase()42     void initTestCase()
43     {
44       QgsApplication::init();
45       QgsApplication::initQgis();
46 
47     }
48 
cleanupTestCase()49     void cleanupTestCase()
50     {
51       QgsApplication::exitQgis();
52     }
53 
testSelectLabel()54     void testSelectLabel()
55     {
56       QgsVectorLayer *vl1 = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=text:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
57       QVERIFY( vl1->isValid() );
58       QgsFeature f1;
59       f1.setAttributes( QgsAttributes() << QStringLiteral( "label" ) );
60       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 1 ) ) );
61       QVERIFY( vl1->dataProvider()->addFeature( f1 ) );
62       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 3 ) ) );
63       f1.setAttributes( QgsAttributes() << QStringLiteral( "l" ) );
64       QVERIFY( vl1->dataProvider()->addFeature( f1 ) );
65       QgsProject::instance()->addMapLayer( vl1 );
66 
67       QgsVectorLayer *vl2 = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=text:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
68       QVERIFY( vl2->isValid() );
69       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 1 ) ) );
70       f1.setAttributes( QgsAttributes() << QStringLiteral( "label" ) );
71       QVERIFY( vl2->dataProvider()->addFeature( f1 ) );
72       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 3 ) ) );
73       f1.setAttributes( QgsAttributes() << QStringLiteral( "label2" ) );
74       QVERIFY( vl2->dataProvider()->addFeature( f1 ) );
75       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 1 ) ) );
76       f1.setAttributes( QgsAttributes() << QStringLiteral( "label3" ) );
77       QVERIFY( vl2->dataProvider()->addFeature( f1 ) );
78       QgsProject::instance()->addMapLayer( vl2 );
79 
80       std::unique_ptr< QgsMapCanvas > canvas = qgis::make_unique< QgsMapCanvas >();
81       canvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3946" ) ) );
82       canvas->setLayers( QList<QgsMapLayer *>() << vl1 << vl2 );
83 
84       QgsMapSettings mapSettings;
85       mapSettings.setOutputSize( QSize( 500, 500 ) );
86       mapSettings.setExtent( QgsRectangle( -1, -1, 4, 4 ) );
87       QVERIFY( mapSettings.hasValidSettings() );
88 
89       mapSettings.setLayers( QList<QgsMapLayer *>() << vl1 << vl2 );
90 
91       canvas->setFrameStyle( QFrame::NoFrame );
92       canvas->resize( 500, 500 );
93       canvas->setExtent( QgsRectangle( -1, -1, 4, 4 ) );
94       canvas->show(); // to make the canvas resize
95       canvas->hide();
96       QCOMPARE( canvas->mapSettings().outputSize(), QSize( 500, 500 ) );
97       QCOMPARE( canvas->mapSettings().visibleExtent(), QgsRectangle( -1, -1, 4, 4 ) );
98 
99       std::unique_ptr< QgsMapToolLabel > tool( new QgsMapToolLabel( canvas.get() ) );
100 
101       // no labels yet
102       QgsPointXY pt;
103       pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
104       std::unique_ptr< QMouseEvent > event( new QMouseEvent(
105                                               QEvent::MouseButtonPress,
106                                               QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
107                                             ) );
108       QgsLabelPosition pos;
109       QVERIFY( !tool->labelAtPosition( event.get(), pos ) );
110 
111       // add some labels
112       QgsPalLayerSettings pls1;
113       pls1.fieldName = QStringLiteral( "text" );
114       pls1.placement = QgsPalLayerSettings::OverPoint;
115       pls1.quadOffset = QgsPalLayerSettings::QuadrantOver;
116       pls1.displayAll = true;
117       QgsTextFormat format = pls1.format();
118       format.setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) );
119       format.setSize( 12 );
120       pls1.setFormat( format );
121 
122       vl1->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
123       vl1->setLabelsEnabled( true );
124 
125       QEventLoop loop;
126       connect( canvas.get(), &QgsMapCanvas::mapCanvasRefreshed, &loop, &QEventLoop::quit );
127       canvas->refreshAllLayers();
128       canvas->show();
129       loop.exec();
130 
131       QVERIFY( canvas->labelingResults() );
132       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
133       QCOMPARE( pos.layerID, vl1->id() );
134       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
135 
136       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
137       event = qgis::make_unique< QMouseEvent >(
138                 QEvent::MouseButtonPress,
139                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
140               );
141       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
142       QCOMPARE( pos.layerID, vl1->id() );
143       QCOMPARE( pos.labelText, QStringLiteral( "l" ) );
144 
145       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
146       event = qgis::make_unique< QMouseEvent >(
147                 QEvent::MouseButtonPress,
148                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
149               );
150       QVERIFY( !tool->labelAtPosition( event.get(), pos ) );
151 
152       // label second layer
153       vl2->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
154       vl2->setLabelsEnabled( true );
155 
156       canvas->refreshAllLayers();
157       canvas->show();
158       loop.exec();
159 
160       // should prioritize current layer
161       canvas->setCurrentLayer( vl1 );
162       pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
163       event = qgis::make_unique< QMouseEvent >(
164                 QEvent::MouseButtonPress,
165                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
166               );
167       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
168       QCOMPARE( pos.layerID, vl1->id() );
169       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
170 
171       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
172       event = qgis::make_unique< QMouseEvent >(
173                 QEvent::MouseButtonPress,
174                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
175               );
176       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
177       QCOMPARE( pos.layerID, vl1->id() );
178       QCOMPARE( pos.labelText, QStringLiteral( "l" ) );
179 
180       //... but fallback to any labels if nothing in current layer
181       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
182       event = qgis::make_unique< QMouseEvent >(
183                 QEvent::MouseButtonPress,
184                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
185               );
186       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
187       QCOMPARE( pos.layerID, vl2->id() );
188       QCOMPARE( pos.labelText, QStringLiteral( "label3" ) );
189 
190       canvas->setCurrentLayer( vl2 );
191       pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
192       event = qgis::make_unique< QMouseEvent >(
193                 QEvent::MouseButtonPress,
194                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
195               );
196       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
197       QCOMPARE( pos.layerID, vl2->id() );
198       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
199 
200       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
201       event = qgis::make_unique< QMouseEvent >(
202                 QEvent::MouseButtonPress,
203                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
204               );
205       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
206       QCOMPARE( pos.layerID, vl2->id() );
207       QCOMPARE( pos.labelText, QStringLiteral( "label2" ) );
208       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
209       event = qgis::make_unique< QMouseEvent >(
210                 QEvent::MouseButtonPress,
211                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
212               );
213       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
214       QCOMPARE( pos.layerID, vl2->id() );
215       QCOMPARE( pos.labelText, QStringLiteral( "label3" ) );
216 
217       canvas->setCurrentLayer( nullptr );
218 
219       // when multiple candidates exist, pick the smallest
220       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
221       event = qgis::make_unique< QMouseEvent >(
222                 QEvent::MouseButtonPress,
223                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
224               );
225       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
226       QCOMPARE( pos.layerID, vl1->id() );
227       QCOMPARE( pos.labelText, QStringLiteral( "l" ) );
228 
229       QgsProject::instance()->clear();
230     }
231 
testAlignment()232     void testAlignment()
233     {
234       QgsVectorLayer *vl1 = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=halig:string&field=valig:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
235       QVERIFY( vl1->isValid() );
236       QgsProject::instance()->addMapLayer( vl1 );
237       QgsFeature f1;
238       f1.setAttributes( QgsAttributes() << QStringLiteral( "right" ) << QStringLiteral( "top" ) );
239       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 1 ) ) );
240       QVERIFY( vl1->dataProvider()->addFeature( f1 ) );
241       f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 3 ) ) );
242       f1.setAttributes( QgsAttributes() << QStringLiteral( "center" ) << QStringLiteral( "base" ) );
243       QVERIFY( vl1->dataProvider()->addFeature( f1 ) );
244 
245       std::unique_ptr< QgsMapCanvas > canvas = qgis::make_unique< QgsMapCanvas >();
246       canvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3946" ) ) );
247       canvas->setLayers( QList<QgsMapLayer *>() << vl1 );
248 
249       QgsMapSettings mapSettings;
250       mapSettings.setOutputSize( QSize( 500, 500 ) );
251       mapSettings.setExtent( QgsRectangle( -1, -1, 4, 4 ) );
252       QVERIFY( mapSettings.hasValidSettings() );
253 
254       mapSettings.setLayers( QList<QgsMapLayer *>() << vl1 );
255 
256       canvas->setFrameStyle( QFrame::NoFrame );
257       canvas->resize( 500, 500 );
258       canvas->setExtent( QgsRectangle( -1, -1, 4, 4 ) );
259       canvas->show(); // to make the canvas resize
260       canvas->hide();
261       QCOMPARE( canvas->mapSettings().outputSize(), QSize( 500, 500 ) );
262       QCOMPARE( canvas->mapSettings().visibleExtent(), QgsRectangle( -1, -1, 4, 4 ) );
263 
264       std::unique_ptr< QgsMapToolLabel > tool( new QgsMapToolLabel( canvas.get() ) );
265 
266       // add some labels
267       QgsPalLayerSettings pls1;
268       pls1.fieldName = QStringLiteral( "'label'" );
269       pls1.isExpression = true;
270       pls1.placement = QgsPalLayerSettings::OverPoint;
271       pls1.quadOffset = QgsPalLayerSettings::QuadrantOver;
272       pls1.displayAll = true;
273       QgsTextFormat format = pls1.format();
274       format.setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) );
275       format.setSize( 12 );
276       pls1.setFormat( format );
277 
278       vl1->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
279       vl1->setLabelsEnabled( true );
280 
281       QEventLoop loop;
282       connect( canvas.get(), &QgsMapCanvas::mapCanvasRefreshed, &loop, &QEventLoop::quit );
283       canvas->refreshAllLayers();
284       canvas->show();
285       loop.exec();
286 
287       QVERIFY( canvas->labelingResults() );
288       QgsPointXY pt;
289       pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
290       std::unique_ptr< QMouseEvent > event( new QMouseEvent(
291                                               QEvent::MouseButtonPress,
292                                               QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
293                                             ) );
294       QgsLabelPosition pos;
295       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
296       QCOMPARE( pos.layerID, vl1->id() );
297       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
298       tool->mCurrentLabel = QgsMapToolLabel::LabelDetails( pos );
299 
300       // defaults to bottom left
301       QString hali, vali;
302       tool->currentAlignment( hali, vali );
303       QCOMPARE( hali, QStringLiteral( "Left" ) );
304       QCOMPARE( vali, QStringLiteral( "Bottom" ) );
305 
306       // using field bound alignment
307       pls1.dataDefinedProperties().setProperty( QgsPalLayerSettings::Hali, QgsProperty::fromField( QStringLiteral( "halig" ) ) );
308       pls1.dataDefinedProperties().setProperty( QgsPalLayerSettings::Vali, QgsProperty::fromField( QStringLiteral( "valig" ) ) );
309       vl1->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
310 
311       canvas->refreshAllLayers();
312       canvas->show();
313       loop.exec();
314 
315       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
316       QCOMPARE( pos.layerID, vl1->id() );
317       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
318       tool->mCurrentLabel = QgsMapToolLabel::LabelDetails( pos );
319 
320       tool->currentAlignment( hali, vali );
321       QCOMPARE( hali, QStringLiteral( "right" ) );
322       QCOMPARE( vali, QStringLiteral( "top" ) );
323 
324       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
325       event = qgis::make_unique< QMouseEvent >(
326                 QEvent::MouseButtonPress,
327                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
328               );
329 
330       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
331       QCOMPARE( pos.layerID, vl1->id() );
332       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
333       tool->mCurrentLabel = QgsMapToolLabel::LabelDetails( pos );
334 
335       tool->currentAlignment( hali, vali );
336       QCOMPARE( hali, QStringLiteral( "center" ) );
337       QCOMPARE( vali, QStringLiteral( "base" ) );
338 
339       // now try with expression based alignment
340       pls1.dataDefinedProperties().setProperty( QgsPalLayerSettings::Hali, QgsProperty::fromExpression( QStringLiteral( "case when $id % 2 = 0 then 'right' else 'left' end" ) ) );
341       pls1.dataDefinedProperties().setProperty( QgsPalLayerSettings::Vali, QgsProperty::fromExpression( QStringLiteral( "case when $id % 2 = 0 then 'half' else 'cap' end" ) ) );
342       vl1->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
343 
344       canvas->refreshAllLayers();
345       canvas->show();
346       loop.exec();
347 
348       pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
349       event = qgis::make_unique< QMouseEvent >(
350                 QEvent::MouseButtonPress,
351                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
352               );
353 
354       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
355       QCOMPARE( pos.layerID, vl1->id() );
356       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
357       tool->mCurrentLabel = QgsMapToolLabel::LabelDetails( pos );
358 
359       tool->currentAlignment( hali, vali );
360       QCOMPARE( hali, QStringLiteral( "left" ) );
361       QCOMPARE( vali, QStringLiteral( "cap" ) );
362 
363       pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
364       event = qgis::make_unique< QMouseEvent >(
365                 QEvent::MouseButtonPress,
366                 QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
367               );
368 
369       QVERIFY( tool->labelAtPosition( event.get(), pos ) );
370       QCOMPARE( pos.layerID, vl1->id() );
371       QCOMPARE( pos.labelText, QStringLiteral( "label" ) );
372       tool->mCurrentLabel = QgsMapToolLabel::LabelDetails( pos );
373 
374       tool->currentAlignment( hali, vali );
375       QCOMPARE( hali, QStringLiteral( "right" ) );
376       QCOMPARE( vali, QStringLiteral( "half" ) );
377     }
378 };
379 
380 QGSTEST_MAIN( TestQgsMapToolLabel )
381 #include "testqgsmaptoollabel.moc"
382